GitHub — Integration Setup
This is not the GitHub Advanced Security integration. That one (
GitHub Advanced Securityin the catalog) reads code-scanning, Dependabot and secret-scanning alerts and needs a paid GHAS licence. This one reads branch protection and merge approvals on plain GitHub, needs no GHAS, and answers a different auditor question: can a change reach production without review? You can connect either, both, or neither.
Prerequisites
- A GitHub organisation (not a personal account — see Known Limitations)
- Permission to create a fine-grained personal access token, or to install a GitHub App
- CMDash Essentials tier or above
Step 1: Create a fine-grained token
- Go to Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.
- Set Resource owner to your organisation.
- Under Repository access, choose All repositories. Coverage percentages are only meaningful over the whole org; a subset silently changes what the number means.
- Under Repository permissions, grant exactly:
- Administration: Read-only — required to read branch protection
- Metadata: Read-only — required to list repositories (GitHub enables this automatically)
- Generate and copy the token.
Least privilege: both permissions are read-only, and CMDash issues only
GETrequests. Do not grant Contents, Actions, or any write scope — nothing in this integration can use them.
Why Administration: branch protection lives under the repository administration API. It is the least-privileged scope that can read it; there is no narrower "read branch protection" permission.
Step 2: Connect in CMDash
- Navigate to Integrations in your CMDash dashboard.
- Find GitHub and click Connect.
- Enter:
- Organisation — the org login exactly as it appears in your URLs (
github.com/acme→acme) - API Token — the fine-grained token from Step 1
- Organisation — the org login exactly as it appears in your URLs (
- Click Save & Test Connection.
CMDash refuses to sync without an organisation rather than falling back to "whatever repositories this token's owner belongs to". That fallback would look like a successful sync while measuring a different population than the one being certified.
What Syncs
| Data Type | GitHub API | Behaviour on failure |
|---|---|---|
| Repository list | GET /orgs/{org}/repos | Fails the sync |
| Default-branch protection, required approvals, admin enforcement | GET /repos/{owner}/{repo}/branches/{branch}/protection | 404 = genuinely unprotected. 403 fails the sync (see below) |
How the numbers are computed
- Branch protection coverage and review-required coverage are over active repositories. Archived repositories are reported but excluded from every denominator — an archive is read-only, so it cannot receive a change, and counting it as unprotected would drag a compliant org's coverage down for repositories nobody can commit to.
- A protected branch is not the same as a reviewed one. A branch can be protected (force-push blocked, for instance) with zero required approvals. CMDash tracks these separately:
branchProtectedandrequiresRevieware different fields, and conflating them would overstate CC8.1 coverage. minRequiredApprovalsis the weakest link — the smallest required-approval count across active repositories, not the average. A mean lets ten well-governed repositories hide the one anybody can merge into, and that one is the entire finding.- A 403 fails the sync; it is never recorded as "unprotected". If the token lacks Administration read, the answer is unknown, and reporting unknown as unprotected would fabricate a control failure for a properly governed repository. A 404, by contrast, is GitHub's documented response for a branch with no protection rule — a real, negative answer.
Controls this evidences
CMDash seeds a suggested mapping for SOC 2 CC8.1 (unprotectedRepositoryCount == 0) and ISO 27001 A.8.32 (reviewRequiredCoverage >= 100).
The CC8.1 rule keys on a count of zero, not a coverage percentage, deliberately: the control asks whether a change can reach production unreviewed, and one ungoverned repository is a yes. A 95%-coverage threshold would pass an organisation with exactly that gap.
Troubleshooting
- Sync fails immediately with "missing its
org": the Organisation field is empty. This is intentional — see Step 2. - 403 with "Refusing to report an unreadable branch as unprotected": the token lacks Administration: Read-only, or the org's token policy has not approved it. Fine-grained tokens on an org often require an org owner to approve the request under Settings → Personal access tokens.
- 404 on the repo list: the org login is wrong, or the token's resource owner is your personal account rather than the org.
- Coverage is lower than expected: check for repositories with protection but zero required reviewers — they are protected but do not require review.
- Repository count is lower than the org shows: the 300-repository ceiling truncated the walk. Check the sync log for the warning.
Rate Limits & Sync Behavior
| Setting | Value |
|---|---|
| Sync frequency | Every 4 hours by default (syncInterval is configurable per connection) |
| Pagination | per_page/page, 100 per page, capped at 300 repositories per sync |
| Request volume | One call for the repo list page, plus one call per active repository — branch protection has no aggregate endpoint. ~301 calls against a 5,000/hour authenticated budget |
| Rate limit handling | Routed through CMDash's shared token-bucket limiter, which paces the per-repository walk |
| Circuit breaker | Repeated failures open the breaker and the sync is skipped |
| Failed sync retry | Job queue retries 3 times with exponential backoff (starting at 30s) — see docs/developers/dead-letter.md |
Credential Rotation
Fine-grained tokens expire (up to 1 year). Note the expiry — an expired token fails every sync with a 401.
To rotate:
- In GitHub: generate a new fine-grained token with the same permissions.
- In CMDash: Settings → Integrations → GitHub → Edit, enter the new token, Save.
- Confirm a successful sync, then revoke the old token.
Known Limitations
- Organisations only. Personal accounts have no
/orgs/{org}/reposequivalent with the same semantics, and coverage across "a person's repositories" is not a meaningful compliance population. - GitHub Enterprise Server is not supported. This adapter targets
api.github.comas a fixed host. A self-hosted GHES instance is a tenant-controlled destination and would need the SSRF-guarded transport GitLab uses. - Only the default branch is inspected. Protection on
release/*or other branches is not read. The default branch is what CC8.1 is about for the overwhelming majority of teams, and inspecting every branch would multiply an already-N+1 walk. - Rulesets are not read. GitHub's newer repository rulesets can enforce review independently of classic branch protection. A repository governed only by a ruleset will currently read as unprotected. This is a real gap, not a design choice — tracked as a follow-up.
- No merge history. CMDash reads the rules, not whether they were ever bypassed. "Was this specific PR approved?" needs the PR API and a retention window; the rule state is the standing control an auditor samples against.
- The 300-repository ceiling is a truncation, not a sample. Larger orgs are cut off and the sync log carries a warning.