Skip to content

chainguard-sandbox/ghcompliance

ghcompliance

GitHub organizations expose many security settings — some governing the organization itself, others controlling individual repositories. Most teams don't have a repeatable way to know whether those settings match their security policy — or to produce evidence that they did at a point in time. This tool captures ~50 of the most security-critical ones and checks them against a policy you write. It never changes any setting.

CI License

OpenSSF Scorecard grades OSS project hygiene (code review culture, signed releases, fuzzing) with hardcoded checks. Allstar enforces settings by mutating them. ghcompliance does neither: it is a read-only auditor against a policy you control.

What it checks

Org-level: 2FA enforcement, default member permission, security manager team count, member repo-creation and private-fork permissions, web commit sign-off, and security defaults applied to new repos (secret scanning, push protection, Dependabot alerts/updates, dependency graph, advanced security).

Repo-level: secret scanning, push protection, Dependabot alerts, advanced security, web commit sign-off, SHA pinning required for Actions, default workflow permissions, fork PR workflow permissions (3 sub-settings), branch protection (approvals, code-owner review, and more), rulesets count, CODEOWNERS presence, and open alert-count thresholds (secret scanning / Dependabot) by severity.

Fine-grained PAT details and code-scanning alert counts are captured in the inventory but are not yet policy-checkable.

Installation

No binary releases yet. Requires Go 1.25+:

git clone https://github.com/chainguard-sandbox/ghcompliance
cd ghcompliance
make build        # produces ./bin/ghcompliance
export GH_TOKEN=<token>

The tool reads GH_TOKEN or GITHUB_TOKEN. Any valid token works — a classic PAT, a fine-grained PAT, a GitHub App installation token, or a value obtained from gh auth token. No gh CLI is required by the binary itself.

Token needs read access to org and repository settings. For GHAS features (secret scanning, Dependabot), the token must belong to an org member with security-setting visibility.

By default, only aggregate counts are collected for identity-level data. Pass --sensitive to also collect collaborator lists (with individual 2FA status), pending invitation email addresses, and fine-grained PAT details (permissions, last-used):

ghcompliance --sensitive inventory ORGNAME > inventory.json

Inventory files and SARIF reports contain security posture data. With --sensitive, they additionally contain PII: individual 2FA status, invitation email addresses, and a full fine-grained PAT inventory including permission scopes and last-used timestamps. Treat these files as confidential and restrict access accordingly.

Large organizations

Classic PATs are limited to 5,000 API calls per hour. ghcompliance uses roughly 25 calls per repository, so a PAT exhausts its budget at around 160 repositories per hourly window. For larger organizations, use a GitHub App installation token, which gets a separate 5,000-call-per-hour budget per installation (15,000 on GHEC).

octo-sts provides keyless GitHub App token exchange via OIDC — no App credentials to manage. In a GitHub Actions workflow:

- uses: octo-sts/action@f603d3be9d8dd9871a265776e625a27b00effe05 # v1.1.1
  id: octo-sts
  with:
    scope: my-org
    identity: ghcompliance-scanner
- name: Inventory
  env:
    GH_TOKEN: ${{ steps.octo-sts.outputs.token }}
  run: ./bin/ghcompliance inventory my-org > inventory.json

When inventorying a specific subset of repos, or when the org is large enough to exhaust a token's hourly budget in a single run, use --repos-file:

# repos.txt: one ORG/REPO per line, up to ~160 repos per 5k-call token
./bin/ghcompliance inventory --repos-file repos.txt > inventory.json

# Evaluate against a policy
./bin/ghcompliance scan inventory.json policy.yaml > report.sarif

The tool budgets 25 API calls per repository and runs a pre-flight check before collecting repos (baseline is 19 calls for a minimal repo; each ruleset and each extra page of alerts adds one more). A 5,000-call hourly budget covers roughly 160 repos before the pre-flight check refuses to start. For orgs larger than that, split repos.txt into chunks of ~150 repos and run once per hourly window, then combine the resulting inventory files before scanning.

Two commands

inventory ORGNAME [ORG/REPO ...] — records actual setting values to JSON on stdout. No policy involved.

scan {ORGNAME | ORG/REPO ... | inventory.json} policy.yaml — evaluates settings against a policy, writes SARIF to stdout. Accepts a live org, named repos, or a prior inventory snapshot.

The split is intentional: one snapshot, many policy evaluations, zero redundant API calls.

Exit codes

Code Meaning
0 Success — scan completed with no policy violations (or inventory written)
1 scan completed and found at least one policy violation (SARIF still written to stdout)
2 Tool error — the run could not complete (bad arguments, missing token, insufficient API quota, API failure)

Only scan returns 1. This lets a CI step gate on violations (exit 1) while still distinguishing them from a tool failure (exit 2).

Policy format

Three rule kinds:

Kind Field Controls
Bool required: true|false two_factor_required, secret_scanning, ...
String allowed_values: [...] default_member_permission, default_workflow_permissions
Count min_count: N security_manager_teams, rulesets

Each rule carries severity (critical / high / medium / low / info), message, and an optional compliance_frameworks map. Framework tags appear as properties on SARIF findings. Unknown field names are rejected at load time.

apiVersion: ghcompliance/v1alpha1
kind: CompliancePolicy
metadata:
  name: example
  organization: my-org
spec:
  organization:
    two_factor_required:
      required: true
      severity: high
      message: "2FA must be enforced org-wide"
      compliance_frameworks:
        SOC2: ["CC6.1"]
        NIST-800-171: ["03.05.03"]
  repository:
    secret_scanning:
      required: true
      severity: critical
      message: "Secret scanning must be enabled"

Starting points: examples/policies/ contains minimal.yaml, comprehensive.yaml, soc2-focused.yaml, cra-focused.yaml, org-defaults.yaml, and repo-override.yaml.

SARIF output

Pipe stdout into github/codeql-action/upload-sarif to surface findings in GitHub Code Scanning. On private repos without GHAS, the upload will fail; the SARIF is still produced.

Development

make check       # fmt, vet, lint, test (race detector)
make check-full  # above + govulncheck

internal/collectorinternal/policyinternal/report. Design specs in docs/spec/; property tests pin the invariants defined there.

License

Apache 2.0 — Copyright 2026 Chainguard, Inc.

About

App for validating GitHub source code controls.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors