Skip to main content
CMDashCybersecurity Maturity Dashboard
← Back to GitHub

GitHub — Integration Setup

This is not the GitHub Advanced Security integration. That one (GitHub Advanced Security in 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

  1. Go to Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.
  2. Set Resource owner to your organisation.
  3. Under Repository access, choose All repositories. Coverage percentages are only meaningful over the whole org; a subset silently changes what the number means.
  4. Under Repository permissions, grant exactly:
    • Administration: Read-only — required to read branch protection
    • Metadata: Read-only — required to list repositories (GitHub enables this automatically)
  5. Generate and copy the token.

Least privilege: both permissions are read-only, and CMDash issues only GET requests. 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

  1. Navigate to Integrations in your CMDash dashboard.
  2. Find GitHub and click Connect.
  3. 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
  4. 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 TypeGitHub APIBehaviour on failure
Repository listGET /orgs/{org}/reposFails the sync
Default-branch protection, required approvals, admin enforcementGET /repos/{owner}/{repo}/branches/{branch}/protection404 = 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: branchProtected and requiresReview are different fields, and conflating them would overstate CC8.1 coverage.
  • minRequiredApprovals is 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

SettingValue
Sync frequencyEvery 4 hours by default (syncInterval is configurable per connection)
Paginationper_page/page, 100 per page, capped at 300 repositories per sync
Request volumeOne 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 handlingRouted through CMDash's shared token-bucket limiter, which paces the per-repository walk
Circuit breakerRepeated failures open the breaker and the sync is skipped
Failed sync retryJob 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:

  1. In GitHub: generate a new fine-grained token with the same permissions.
  2. In CMDash: Settings → Integrations → GitHub → Edit, enter the new token, Save.
  3. Confirm a successful sync, then revoke the old token.

Known Limitations

  • Organisations only. Personal accounts have no /orgs/{org}/repos equivalent 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.com as 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.

Ready to see your posture?

Start a 30-day free trial of any paid plan — a card is required, and it converts unless you cancel — or sign up for Free with no card at all.