Evidence kitplatforms/github.md
Platforms

GitHub evidence guide

Where to find each piece of controls evidence in GitHub — UI paths, gh CLI commands, exports that satisfy auditors.

Per-control reference for gathering compliance evidence in GitHub. Most engineering-led SOC 2 readiness work has GitHub as the critical-control surface for code review, deploy approvals, and secret scanning.

1. Access controls

Org membership + MFA (control 1.1)

UI: Org → People → Outside collaborators tab. Org → Settings → Authentication security → "Require two-factor authentication".

CLI:

# Org-level 2FA enforcement
gh api orgs/<org> --jq '.two_factor_requirement_enabled'

# Members with 2FA disabled (requires admin scope)
gh api "orgs/<org>/members?filter=2fa_disabled" --jq '.[].login'

# Org owners
gh api orgs/<org>/members --jq 'map(select(.role=="admin"))'

Evidence: confirm two_factor_requirement_enabled = true org-wide. This is the right control; it prevents anyone from being a member without 2FA.

Personal Access Tokens

PATs are a common blind spot. Even with 2FA, a leaked PAT lets an attacker bypass it.

# Audit log entries for PAT creation (Enterprise only)
gh api orgs/<org>/audit-log --jq 'map(select(.action == "user.token_creation"))'

# OAuth apps + GitHub Apps installed on the org
gh api orgs/<org>/installations --jq '.installations[].account.login'

Best practice: replace PATs with GitHub Apps (org-installable, auditable, fine-grained permissions). Document any remaining PATs with owner + rotation date.

CODEOWNERS (control 3.1 dependency)

# Pull the canonical CODEOWNERS
gh api repos/<owner>/<repo>/contents/.github/CODEOWNERS \
  --jq '.content' | base64 -d

3. Change management

Branch protection / Rulesets (control 3.1)

GitHub has two systems: legacy "Branch Protection" and newer "Rulesets" (more flexible, org-applicable). Rulesets are the direction; both are auditor-acceptable.

# Legacy branch protection
gh api repos/<owner>/<repo>/branches/main/protection > branch-protection-main.json

# Newer rulesets
gh api repos/<owner>/<repo>/rulesets > rulesets.json
gh api repos/<owner>/<repo>/rulesets/<id> > ruleset-detail.json

# Org-level rulesets (apply to all repos in the org)
gh api orgs/<org>/rulesets > org-rulesets.json

Org-level rulesets are the strongest evidence — they apply the same protection across every repository, so adding a new repo doesn't accidentally create an unprotected production branch.

Environment protection (control 3.2)

# Environments on a repo
gh api repos/<owner>/<repo>/environments > environments.json

# Production env detail (required reviewers, deployment branch
# restrictions, secrets scope)
gh api repos/<owner>/<repo>/environments/production > env-production.json
gh api repos/<owner>/<repo>/environments/production/deployment-protection-rules > prod-protection-rules.json

Deploy history (control 3.4)

# Last N workflow runs on main
gh run list --workflow=deploy.yml --branch=main --limit=30 \
  --json databaseId,displayTitle,headSha,createdAt,actor,conclusion \
  > recent-deploys.json

# Per-deploy approver
gh api repos/<owner>/<repo>/actions/runs/<id>/approvals

Signed commits (bonus credibility)

# Branch protection rule: require signed commits
gh api repos/<owner>/<repo>/branches/main/protection \
  --jq '.required_signatures.enabled'

# Verify a specific commit
gh api repos/<owner>/<repo>/commits/<sha> --jq '.commit.verification'

4. Vulnerability + secret scanning

Dependabot (control 4.1)

# Dependabot config in repo
gh api repos/<owner>/<repo>/contents/.github/dependabot.yml \
  --jq '.content' | base64 -d

# Dependabot alerts
gh api repos/<owner>/<repo>/dependabot/alerts \
  --jq '[.[] | {number, severity, state, package: .dependency.package.name, fix_resolved: .fixed_at, created_at}]' \
  > dependabot-alerts.json

Secret scanning + push protection (control 4.3)

# Org-wide secret scanning + push protection
gh api orgs/<org> --jq '.security_and_analysis'

# Per-repo
gh api repos/<owner>/<repo> --jq '.security_and_analysis'

# Open alerts
gh api repos/<owner>/<repo>/secret-scanning/alerts \
  --jq '[.[] | {number, secret_type, state, resolution, created_at}]'

The control that matters: secret_scanning_push_protection set to enabled org-wide. Push protection prevents the secret from reaching the repo in the first place.

Code scanning (CodeQL / SAST)

# Enable
gh api repos/<owner>/<repo>/code-scanning/default-setup -X PUT \
  --field state=configured

# Alerts
gh api repos/<owner>/<repo>/code-scanning/alerts \
  --jq '[.[] | {number, rule_id, severity, state}]'

Audit log

GitHub Enterprise / GHE Cloud has an org-level audit log API. Pull this quarterly for the compliance evidence trail:

gh api "orgs/<org>/audit-log?phrase=created:>2026-04-01" \
  --paginate > audit-log-2026-Q2.json

This is gold for "who did what" reconstruction during an incident.

Authoritative references

Book review