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.
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.
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.
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.jsonInventory 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.
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.jsonWhen 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.sarifThe 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.
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.
| 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).
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.
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.
make check # fmt, vet, lint, test (race detector)
make check-full # above + govulncheck
internal/collector → internal/policy → internal/report. Design specs in docs/spec/; property tests pin the invariants defined there.
Apache 2.0 — Copyright 2026 Chainguard, Inc.