Documentation
¶
Index ¶
- Constants
- Variables
- func GenerateReport(sr ScanResult) string
- func IsRateLimitError(err error) bool
- func Score(sr ScanResult) (score int, defined bool)
- type Auth
- type Bucket
- type FileEntry
- type GitHubClient
- type HasActivity
- type HasBranchProtection
- type HasCIWorkflow
- type HasCodeowners
- type HasLicense
- type HasReadme
- type HasRepoDescription
- type HasRequiredChecks
- type HasRequiredReviewers
- type HasSecurityMd
- type InstallationAuth
- type MergeGate
- type Option
- type PATAuth
- type Repo
- type RepoResult
- type Rule
- type RuleCategory
- type RuleResult
- type ScanResult
Constants ¶
const Version = "v0.9.4"
Version is the semver tag of this scanner library. Bumped manually per release. Surfaced in the report header so a reader can see which scanner version produced a given scorecard.
Variables ¶
var ( ErrEmptyRepo = errors.New("repository is empty") ErrTruncatedTree = errors.New("tree truncated by GitHub API") )
Sentinel errors for per-repo scan failures.
Functions ¶
func GenerateReport ¶
func GenerateReport(sr ScanResult) string
GenerateReport produces a Markdown repo-standards scorecard from a ScanResult. The structure is fixed and meaningful for prospects landing from a cold-email link:
- Header: title, org, scan time, single-line repo stats
- ## Scored rules table (importance order, drives the score)
- **Score: N/100** inline callout (or **Score: N/A** when no repos)
- ## Additional checks table (importance order, same columns as scored)
- ## Rule reference (collapsed <details>, split by category)
- ## Repository details: ### Strong / Moderate / Weak / Skipped subsections
Rule reference precedes Repository details so a reader scanning top-down has the rule definitions in hand before they hit the per-repo failure lists, which only mention rule names.
func IsRateLimitError ¶ added in v0.6.0
IsRateLimitError reports whether an error is a GitHub rate limit error (primary or secondary). Rate limit errors must never be swallowed - they indicate a global problem that affects all subsequent API calls. Exported so callers (e.g., bulk-scan) can decide whether to abort a multi-org run on the first rate-limited org rather than continue and fail every subsequent call.
func Score ¶ added in v0.7.0
func Score(sr ScanResult) (score int, defined bool)
Score computes the org-level score: the arithmetic mean of pass rates across sr.RulesScored. Returns the score (0-100) and a flag indicating whether it's defined. When sr has no scanned repos OR no scored rules were evaluated (e.g., a non-admin scan with admin-only rules filtered out and no scored rules left), defined=false and the caller should render "N/A". Result is rounded to the nearest integer for display.
The denominator is len(sr.RulesScored), not the size of the global scored-rule set - that's how non-admin scans get the math right without rules they couldn't evaluate dragging the score down.
Types ¶
type Auth ¶ added in v0.3.0
type Auth interface {
// contains filtered or unexported methods
}
Auth identifies how the scanner authenticates to GitHub. It is a sealed interface — only PATAuth and InstallationAuth in this package satisfy it. New auth types are added by defining a struct with an isAuth() method.
type Bucket ¶ added in v0.7.0
type Bucket struct {
Name string // "Strong", "Moderate", "Weak"
MinPct int // inclusive lower bound (0..100)
MaxPct int // inclusive upper bound (0..100)
}
Bucket classifies a repo by what fraction of the scored rules it passes. Each bucket covers an integer percentage range; the full bucket set returned by Buckets() covers [0, 100] without gaps or overlaps. Display labels are derived from MinPct/MaxPct at render time (see report.go).
func BucketOf ¶ added in v0.7.0
func BucketOf(rr RepoResult, scoredRules []Rule) (b Bucket, scoredPassing, scoredTotal, scorePct int)
BucketOf classifies a single repo by the percentage of scored rules it passes. The caller passes the scored-rule set so the denominator is stable across the org's scan: every repo gets the same denominator, regardless of which rules happen to appear in any one repo's results. Pass sr.RulesScored from the parent ScanResult.
Returns the matching Bucket plus the underlying counts so callers don't re-derive them. If scoredRules is empty the result is the last-defined bucket (i.e. Weak) with zero counts; this only happens in test fixtures with no scored rules registered.
func Buckets ¶ added in v0.7.0
func Buckets() []Bucket
Buckets returns the score-range buckets in display order (highest range first). Adding/removing buckets, renaming them, or shifting thresholds is a one-place edit here - report and stats output both derive from this list and need no separate updates.
type FileEntry ¶
type FileEntry struct {
Path string // full path relative to repo root (e.g., ".github/workflows/ci.yml")
Size int
Type string // "blob" (file) or "tree" (directory)
}
FileEntry represents a file or directory in a repo.
type GitHubClient ¶
type GitHubClient interface {
// ListReposByAccount lists repos for a named org (falls back to user on 404).
// Used by PAT auth.
ListReposByAccount(ctx context.Context, name string) ([]Repo, error)
// ListReposByInstallation lists the repos the current GitHub App installation
// was granted access to. Used by installation-token auth.
ListReposByInstallation(ctx context.Context) ([]Repo, error)
GetTree(ctx context.Context, owner, repo, branch string) ([]FileEntry, error)
GetBranchProtection(ctx context.Context, owner, repo, branch string) (*MergeGate, error)
GetRulesets(ctx context.Context, owner, repo, branch string) (*MergeGate, error)
// GetBranchInfo reads the public GET /repos/{o}/{r}/branches/{br}
// endpoint, which exposes the protected flag and (for classic
// per-repo branch protection) the required-status-check contexts to
// any reader - including non-admins on public repos. This is the
// fallback when the admin GetBranchProtection 404s and there are no
// rulesets, so the scanner can still tell whether protection is on
// and which status checks are required. Required-reviewer counts
// are NOT exposed here (admin-only field on classic protection).
GetBranchInfo(ctx context.Context, owner, repo, branch string) (*MergeGate, error)
}
GitHubClient is the interface for all GitHub API interactions. The scanner depends only on this interface, making it testable via mocks.
func NewGitHubClient ¶
func NewGitHubClient(token string) GitHubClient
NewGitHubClient creates a GitHubClient that calls the public GitHub REST API.
type HasActivity ¶ added in v0.5.1
HasActivity checks that the repo has had a commit (push) within the last 12 months. Set Now to a fixed time for deterministic testing; the zero value means time.Now() is used at check time.
func (HasActivity) Category ¶ added in v0.7.0
func (r HasActivity) Category() RuleCategory
func (HasActivity) Check ¶ added in v0.5.1
func (r HasActivity) Check(repo Repo) bool
func (HasActivity) Description ¶ added in v0.5.1
func (r HasActivity) Description() string
func (HasActivity) Name ¶ added in v0.5.1
func (r HasActivity) Name() string
type HasBranchProtection ¶
type HasBranchProtection struct{}
HasBranchProtection checks that the default branch enforces PR-flow: direct pushes blocked, merges go through a PR. One of three rules derived from MergeGate; the other two are HasRequiredReviewers and HasRequiredChecks. See MergeGate (client.go) and resolveMergeGate (scanner.go) for how the signal is built.
func (HasBranchProtection) Category ¶ added in v0.7.0
func (r HasBranchProtection) Category() RuleCategory
func (HasBranchProtection) Check ¶
func (r HasBranchProtection) Check(repo Repo) bool
func (HasBranchProtection) Description ¶ added in v0.4.0
func (r HasBranchProtection) Description() string
func (HasBranchProtection) Name ¶
func (r HasBranchProtection) Name() string
type HasCIWorkflow ¶
type HasCIWorkflow struct{}
HasCIWorkflow checks that the repo has a CI workflow configured for any of the well-known CI providers, not just GitHub Actions. Detected via the presence of one of these signals at the repo root or under their canonical directory:
- GitHub Actions: .github/workflows/*.yml or *.yaml
- CircleCI: .circleci/config.yml
- GitLab CI: .gitlab-ci.yml
- Travis CI: .travis.yml
- Buildkite: any file under .buildkite/
- Azure Pipelines: azure-pipelines.yml
- Jenkins: Jenkinsfile
Repos using a CI integration that lives entirely server-side (e.g., CircleCI without a checked-in config) are still missed; this is a best-effort signal based on what's visible in the repo.
func (HasCIWorkflow) Category ¶ added in v0.7.0
func (r HasCIWorkflow) Category() RuleCategory
func (HasCIWorkflow) Check ¶
func (r HasCIWorkflow) Check(repo Repo) bool
func (HasCIWorkflow) Description ¶ added in v0.4.0
func (r HasCIWorkflow) Description() string
func (HasCIWorkflow) Name ¶
func (r HasCIWorkflow) Name() string
type HasCodeowners ¶
type HasCodeowners struct{}
HasCodeowners checks that a CODEOWNERS file exists in root, docs/, or .github/.
func (HasCodeowners) Category ¶ added in v0.7.0
func (r HasCodeowners) Category() RuleCategory
func (HasCodeowners) Check ¶
func (r HasCodeowners) Check(repo Repo) bool
func (HasCodeowners) Description ¶ added in v0.4.0
func (r HasCodeowners) Description() string
func (HasCodeowners) Name ¶
func (r HasCodeowners) Name() string
type HasLicense ¶
type HasLicense struct{}
HasLicense uses GitHub's auto-detected license (Licensee) instead of a path-pattern match, so any conventionally-named license file works: LICENSE, LICENSE.md, LICENSE.txt, LICENCE (British), COPYING (GNU), MIT-LICENSE, etc. - anything GitHub recognizes and surfaces as the repo's `license.spdx_id` in the listing payload.
Custom-text licenses GitHub can't auto-detect won't pass even though the file may be present. That's a known false negative; the trade-off is worth it for the much broader correct-positive coverage.
func (HasLicense) Category ¶ added in v0.7.0
func (r HasLicense) Category() RuleCategory
func (HasLicense) Check ¶
func (r HasLicense) Check(repo Repo) bool
func (HasLicense) Description ¶ added in v0.4.0
func (r HasLicense) Description() string
func (HasLicense) Name ¶
func (r HasLicense) Name() string
type HasReadme ¶ added in v0.7.0
type HasReadme struct{}
HasReadme checks that some form of README file exists at the repo root. Matches case-insensitively on the filename and accepts any extension (or no extension), so README.md, readme.rst, README.txt, Readme, README.markdown all pass. Subdirectory READMEs (e.g., docs/README.md) don't count - the rule is about a top-level project README.
(No size threshold - the previous "substantial" variant was dropped because 2 KB is too low to discriminate quality and too high to reward minimal but useful READMEs.)
func (HasReadme) Category ¶ added in v0.7.0
func (r HasReadme) Category() RuleCategory
func (HasReadme) Description ¶ added in v0.7.0
type HasRepoDescription ¶
type HasRepoDescription struct{}
HasRepoDescription checks that the repo description field is not blank.
func (HasRepoDescription) Category ¶ added in v0.7.0
func (r HasRepoDescription) Category() RuleCategory
func (HasRepoDescription) Check ¶
func (r HasRepoDescription) Check(repo Repo) bool
func (HasRepoDescription) Description ¶ added in v0.4.0
func (r HasRepoDescription) Description() string
func (HasRepoDescription) Name ¶
func (r HasRepoDescription) Name() string
type HasRequiredChecks ¶ added in v0.8.4
type HasRequiredChecks struct{}
HasRequiredChecks checks that the default branch requires at least one programmatic check (CI status check, workflow, code scan, deployment, etc.) to pass before merging. Reads MergeGate.RequiredStatusChecks, which resolveMergeGate populates from whichever sources ran.
func (HasRequiredChecks) Category ¶ added in v0.8.4
func (r HasRequiredChecks) Category() RuleCategory
func (HasRequiredChecks) Check ¶ added in v0.8.4
func (r HasRequiredChecks) Check(repo Repo) bool
func (HasRequiredChecks) Description ¶ added in v0.8.4
func (r HasRequiredChecks) Description() string
func (HasRequiredChecks) Name ¶ added in v0.8.4
func (r HasRequiredChecks) Name() string
type HasRequiredReviewers ¶
type HasRequiredReviewers struct{}
HasRequiredReviewers checks that at least one approving review is required. Admin-only: non-admin scans skip the rule entirely because the classic-protection reviewer count is admin-only and answering only for rulesets-only repos would produce misleading partial coverage. See effectiveRules in scanner.go.
func (HasRequiredReviewers) Category ¶ added in v0.7.0
func (r HasRequiredReviewers) Category() RuleCategory
func (HasRequiredReviewers) Check ¶
func (r HasRequiredReviewers) Check(repo Repo) bool
func (HasRequiredReviewers) Description ¶ added in v0.4.0
func (r HasRequiredReviewers) Description() string
func (HasRequiredReviewers) Name ¶
func (r HasRequiredReviewers) Name() string
func (HasRequiredReviewers) RequiresAdmin ¶ added in v0.8.0
func (r HasRequiredReviewers) RequiresAdmin() bool
type HasSecurityMd ¶
type HasSecurityMd struct{}
HasSecurityMd checks that SECURITY.md exists in any of the three locations GitHub recognizes for security policies: repo root, .github/, or docs/.
func (HasSecurityMd) Category ¶ added in v0.7.0
func (r HasSecurityMd) Category() RuleCategory
func (HasSecurityMd) Check ¶
func (r HasSecurityMd) Check(repo Repo) bool
func (HasSecurityMd) Description ¶ added in v0.4.0
func (r HasSecurityMd) Description() string
func (HasSecurityMd) Name ¶
func (r HasSecurityMd) Name() string
type InstallationAuth ¶ added in v0.3.0
type InstallationAuth struct {
Token string
Name string // org or user login the app is installed on (used in repo URLs)
}
InstallationAuth uses a GitHub App installation access token. Scanner lists repositories via /installation/repositories, which returns exactly the repos the installation was granted access to (no public-repo leak on "Selected repositories" installs).
type MergeGate ¶ added in v0.9.2
type MergeGate struct {
EnforcesPRFlow bool // direct pushes blocked; merges go through a PR
RequiredReviewers int // approving reviewers required to merge
RequiredStatusChecks []string // identifiers of required merge-gate checks
}
MergeGate holds the merge requirements the scanner extracts from GitHub's branch-protection APIs. The struct is the union of whichever sources ran for the scan mode; see resolveMergeGate (scanner.go) for the source matrix and per-source signals. A nil *MergeGate means no merge requirements were found.
type Option ¶ added in v0.2.0
type Option func(*scanOptions)
Option configures optional scan behavior.
func WithAdmin ¶ added in v0.8.0
WithAdmin signals that the auth has admin access on every repo it can see. When true, the scanner runs all rules, including those that need admin-only API endpoints (currently: required-reviewers visibility on classic per-repo branch protection). When false (the default), rules marked admin-only are silently skipped - they don't appear in the per-repo results, the JSON output, or the Markdown report. Their absence is invisible to downstream consumers, who simply don't see those keys/columns.
Pass true when scanning with an installation token issued by the Codatus GitHub App (which is granted admin) or a PAT belonging to an admin of every target org. Pass false (or leave default) for third-party / public scans where admin signals can't be read.
func WithBaseURL ¶ added in v0.2.0
WithBaseURL sets a custom GitHub API base URL. Defaults to the public GitHub API when unset. Useful for testing against a mock server or pointing at a GitHub Enterprise instance.
type PATAuth ¶ added in v0.3.0
PATAuth uses a Personal Access Token targeting a named account. Scanner lists repositories via /orgs/{Name}/repos and falls back to /users/{Name}/repos on 404, so it works for both org and user accounts.
type Repo ¶
type Repo struct {
Name string
Description string
DefaultBranch string
Archived bool
Fork bool
PushedAt time.Time // most recent push to any branch (from list-repos)
License string // SPDX id GitHub auto-detected (Licensee), "" if none
Files []FileEntry // all files and directories in the repo
MergeGate *MergeGate // nil if no merge requirements were found
}
Repo represents a GitHub repository with the fields the scanner needs.
type RepoResult ¶
type RepoResult struct {
RepoName string
MostRecentCommit time.Time // PushedAt from the listing; zero if unknown
Results []RuleResult
KnownSkipReason string
UnknownSkipError string
}
RepoResult holds all rule results for a single repository. KnownSkipReason and UnknownSkipError are mutually exclusive.
func (RepoResult) Skipped ¶ added in v0.2.0
func (rr RepoResult) Skipped() bool
type Rule ¶
type Rule interface {
Name() string
Category() RuleCategory
Check(repo Repo) bool
Description() string
}
Rule defines a named check that produces a pass/fail result for a repo. Description supplies the per-rule text used by the Markdown scorecard's Rule reference section: a single self-contained paragraph that names what's checked, every detection path the rule walks, and how to fix it. Category determines whether the rule feeds into the org-level score or appears in the informational-only "Additional checks" section.
func AdditionalRules ¶ added in v0.7.0
func AdditionalRules() []Rule
AdditionalRules returns just the rules with CategoryAdditional, in AllRules order.
func AllRules ¶
func AllRules() []Rule
AllRules returns the ordered list of rules the scanner evaluates. The order is fixed and meaningful: scored rules first (by importance), then additional checks (by importance). Callers that want only one category can use ScoredRules or AdditionalRules.
func ScoredRules ¶ added in v0.7.0
func ScoredRules() []Rule
ScoredRules returns just the rules with CategoryScored, in AllRules order.
type RuleCategory ¶ added in v0.7.0
type RuleCategory string
RuleCategory classifies a rule as either a *scored* rule (contributes to the org-level score) or an *additional* check (informational only).
const ( CategoryScored RuleCategory = "scored" CategoryAdditional RuleCategory = "additional" )
type RuleResult ¶
RuleResult holds the outcome of a single rule check for a single repo.
type ScanResult ¶ added in v0.6.0
type ScanResult struct {
Org string
ScannedAt time.Time
TotalRepos int // total repos returned by GitHub before any filtering
ArchivedExcluded int // archived repos filtered out at listing time
ForksExcluded int // forked repos filtered out at listing time
Skipped []RepoResult // empty repos, truncated trees, or unexpected errors during the scan
Results []RepoResult // repos that finished scanning (success or fail per-rule)
// RulesScored and RulesAdditional are the rules actually run against
// each repo, split by category. They reflect WithAdmin filtering: an
// admin-only rule skipped on a non-admin scan does NOT appear here,
// so all downstream math (Score, BucketOf, table aggregation) is
// driven directly by these slices instead of inferring evaluated
// rules from RepoResult.Results.
//
// JSON-tagged "-" because Rule is an interface and consumers that
// marshal a ScanResult should instead build their own per-rule
// payload (see cmd/bulk-scan for an example). The fields are stable
// for in-process use only.
RulesScored []Rule `json:"-"`
RulesAdditional []Rule `json:"-"`
}
ScanResult bundles the scan outcome with the listing-time exclusion counts the scanner accumulates while filtering archived and forked repos. The counts let callers report a full breakdown ("32 total, 4 forks excluded, 2 archived excluded, 26 scanned") without re-querying GitHub.
The library does not expose a precomputed "most recent commit across the org" — each RepoResult carries its own MostRecentCommit and consumers aggregate as needed.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
bulk-scan
command
bulk-scan reads a list of GitHub orgs/users from a file, runs the scanner against each one, and writes per-org output files (scorecard.md + stats.json) into a destination folder.
|
bulk-scan reads a list of GitHub orgs/users from a file, runs the scanner against each one, and writes per-org output files (scorecard.md + stats.json) into a destination folder. |
|
generate-sample
command
generate-sample renders samples.Fixture() through scanner.GenerateReport and writes the resulting Markdown to stdout (or to a file via --out).
|
generate-sample renders samples.Fixture() through scanner.GenerateReport and writes the resulting Markdown to stdout (or to a file via --out). |
|
scanner
command
|
|
|
Package samples provides the canonical sample scorecard used to drive the landing page hero and the app's dev-seed data.
|
Package samples provides the canonical sample scorecard used to drive the landing page hero and the app's dev-seed data. |