Computed Branch Access Edges
Overview
The GitHub collectors compute effective branch push access as a post-collection step after branches and branch protection rules have been collected and before workflow analysis. In GitHound, this logic is implemented byCompute-GitHoundBranchAccess.
Why it exists: The raw permission edges in the graph (GH_WriteRepoContents, GH_PushProtectedBranch, GH_BypassBranchProtection) are each necessary but not sufficient for push access. A user with GH_WriteRepoContents may be blocked by branch protection rules, while a user with GH_PushProtectedBranch only bypasses push restrictions (not PR reviews). Determining whether someone can actually push requires cross-referencing role permissions, branch protection rule settings, per-rule allowances, and enforce_admins state. This function performs that analysis and emits computed edges that represent verified push capability.
Key characteristics:
- Pure in-memory computation — no API calls
- Operates over the full accumulated node and edge collections from prior steps
- Produces only edges (no new nodes)
Edge Kinds Produced
Most edges emit from
GH_RepoRole. Per-actor edges from GH_User/GH_Team are only emitted when per-rule allowances (pushAllowances, bypassPullRequestAllowances) grant access beyond what the role provides.
Reason Values
Each computed edge includes areason property explaining why access was granted:
Composition Queries
Each computed edge includes aquery_composition property containing a Cypher query that reveals the underlying graph elements that caused the edge to be created.
The Two-Gate Model
The computation evaluates two independent gates per branch. An actor must pass both gates to push.Merge Gate
Active whenrequired_pull_request_reviews or lock_branch is true on the protecting BPR.
Push Gate
Active whenpush_restrictions is true on the protecting BPR.
The asymmetry is critical:
enforce_admins only suppresses merge-gate bypasses. Admin users and users with push_protected_branch can always bypass push restrictions regardless of enforce_admins.
Relationship to Raw Permission Edges
The raw permission edges remain in the graph for detailed analysis:
The computed edges (
GH_CanCreateBranch, GH_CanWriteBranch) are traversable because they represent verified push capability after evaluating all gates and bypass mechanisms.
Algorithm
The computation operates in three phases.Phase 1: Index Building
Constructs lookup structures from the raw node and edge collections for O(1) access during evaluation. Node and edge indexes:
Domain-specific indexes:
Phase 2: Role Permission Resolution
Builds full permission sets for all roles by traversing theGH_HasBaseRole inheritance chain.
Get-BaseRolePerms performs a forward-transitive closure: given a role, follows outbound GH_HasBaseRole edges to collect all inherited permissions. For example:
Phase 3a: Role-Level Edge Emission
For each repository and each write-capable role, evaluates whether the role’s permissions alone are sufficient to bypass branch protection. GH_CanEditProtection: If the role hasGH_EditRepoProtections or GH_AdminTo, emit an edge from the role to each protected branch on the repo.
GH_CanCreateBranch: Evaluates whether the role can create new branches by checking for a wildcard (*) BPR with both push_restrictions and blocks_creations enabled:
- No wildcard blocking BPR → emit
role → repo(reason:no_protection) - Wildcard BPR exists + role has admin → emit
role → repo(reason:admin) - Wildcard BPR exists + role has
push_protected_branch→ emitrole → repo(reason:push_protected_branch) - Otherwise → no edge
- Look up the protecting BPR (if any)
- Evaluate the merge gate — blocked unless bypassed by admin or
bypass_branch_protection(both suppressed byenforce_admins) - Evaluate the push gate — blocked unless bypassed by admin or
push_protected_branch(neither affected byenforce_admins) - Branch is accessible only if both gates pass
Phase 3b: Per-Actor Allowance Delta
Per-rule allowances (pushAllowances, bypassPullRequestAllowances) are actor-specific — they grant access to individual users or teams, not to roles. This phase computes the delta: branches an actor can access via allowances that their role alone doesn’t cover.
For each actor in any per-rule allowance on a repository:
- Compute covered branches: Union of role-accessible branches across all leaf roles the actor reaches
- Prerequisite check: The actor must have write access (via their role) to the repo. Allowances don’t grant write access — they only modify which branches a writer can push to
- GH_CanCreateBranch delta: If a wildcard blocking BPR exists and the actor’s role doesn’t grant
GH_CanCreateBranch, check if the actor is inpushAllowancesfor the wildcard BPR. If so, emitactor → repo(reason:push_allowance) - GH_CanWriteBranch delta: For each branch not covered by the actor’s role, re-evaluate both gates considering the actor’s allowance memberships. If both gates pass, emit
actor → branch
Edge Deduplication
An$emittedEdges hashtable keyed by "startId|endId|kind" prevents duplicate edges when multiple code paths could emit the same edge.
Graph Traversal Paths
Role-level (common case):Computed Secret Scanning Access Edges
Overview
The GitHub collectors compute effective secret scanning alert read access as a post-collection step after secret scanning alerts have been collected and before app installation analysis. In GitHound, this logic is implemented byCompute-GitHoundSecretScanningAccess.
Why it exists: The raw GH_ViewSecretScanningAlerts permission edges connect roles to organizations or repositories, but do not connect roles directly to the individual alert nodes. Without computed edges, BloodHound pathfinding cannot traverse from a role to the alert (and onward via GH_ValidToken to the compromised user identity). This function bridges that gap by resolving which specific alerts each role can read.
Key characteristics:
- Pure in-memory computation — no API calls
- Produces only edges (no new nodes)
- Simpler than branch access computation — no gate evaluation needed
Edge Kind Produced
Reason Values
Algorithm
Phase 1: Index Building
Constructs lookup structures from the raw node and edge collections.$alertNodeIds— HashSet of allGH_SecretScanningAlertnode IDs$orgAlerts— maps each org ID to its contained alert IDs$repoAlerts— maps each repo ID to its contained alert IDsGH_ViewSecretScanningAlertsedges are split into$orgViewEdges(target isGH_Organization) and$repoViewEdges(target isGH_Repository)
Phase 2: Org-Level Emission
For eachGH_ViewSecretScanningAlerts edge targeting a GH_Organization:
- Get the source role ID and target org ID
- Look up all alerts in that org
- For each alert: emit
GH_CanReadSecretScanningAlertfrom the org role to the alert (reason:org_role_permission)
Phase 3: Repo-Level Emission
For eachGH_ViewSecretScanningAlerts edge targeting a GH_Repository:
- Get the source role ID and target repo ID
- Look up all alerts in that repo
- For each alert: emit
GH_CanReadSecretScanningAlertfrom the repo role to the alert (reason:repo_role_permission)
Security Significance
This edge completes a critical attack path: an actor who can view secret scanning alerts gains access to the raw leaked secret values. When the leaked secret is a valid GitHub Personal Access Token (detected by theGH_ValidToken edge), the actor can impersonate the token owner and exercise all permissions granted to that token.
Complete attack path: