Computed Branch Access Edges
Overview
Compute-GitHoundBranchAccess is a post-collection step that computes effective branch push access. It runs as Step 6.5 in Invoke-GitHound, after branches and branch protection rules have been collected and before workflows are collected.
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
| Edge Kind | Source | Target | Traversable | Description |
|---|---|---|---|---|
GH_CanCreateBranch | GH_RepoRole | GH_Repository | Yes | Role can create new branches |
GH_CanCreateBranch | GH_User or GH_Team | GH_Repository | Yes | Per-rule allowance delta — actor can create branches when role alone doesn’t grant access |
GH_CanWriteBranch | GH_RepoRole | GH_Branch | Yes | Role can push to this specific branch |
GH_CanWriteBranch | GH_User or GH_Team | GH_Branch | Yes | Per-rule allowance delta — actor can push when role alone doesn’t grant access |
GH_CanEditProtection | GH_RepoRole | GH_Branch | Yes | Role can modify/remove the BPR(s) governing this branch |
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:
| Reason | Meaning |
|---|---|
no_protection | No branch protection rule applies to this branch |
admin | Admin access bypasses the gate |
push_protected_branch | Role has push_protected_branch permission (bypasses push gate) |
bypass_branch_protection | Role has bypass_branch_protection permission (bypasses merge gate) |
push_allowance | Actor is in pushAllowances for the matching BPR |
bypass_pr_allowance | Actor is in bypassPullRequestAllowances (bypasses PR reviews only) |
edit_repo_protections | Role can modify/remove this BPR (used on GH_CanEditProtection edges) |
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.
| Edge Type | Source | What the query shows |
|---|---|---|
GH_CanWriteBranch | RepoRole → Branch | Role’s permission edges + BPR protecting the branch |
GH_CanCreateBranch | RepoRole → Repository | Role’s permission edges + wildcard BPR (if any) |
GH_CanEditProtection | RepoRole → Branch | Role’s edit/admin permission edge + repo’s branches + protecting BPR(s) |
GH_CanWriteBranch | User/Team → Branch | Actor’s allowance edges to the BPR + actor’s role path with permissions |
GH_CanCreateBranch | User/Team → Repository | Actor’s push allowance to the wildcard BPR + actor’s role path |
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.
| Bypass Mechanism | Scope | Suppressed by enforce_admins? |
|---|---|---|
GH_AdminTo (admin access) | Role-level | Yes |
GH_BypassBranchProtection | Role-level | Yes |
bypassPullRequestAllowances | Per-actor | Yes (PR reviews only, does not bypass lock_branch) |
Push Gate
Active whenpush_restrictions is true on the protecting BPR.
| Bypass Mechanism | Scope | Suppressed by enforce_admins? |
|---|---|---|
GH_AdminTo (admin access) | Role-level | No |
GH_PushProtectedBranch | Role-level | No |
pushAllowances | Per-actor | No |
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:| Raw Edge | Traversable | Why not traversable |
|---|---|---|
GH_WriteRepoContents | No | Necessary but not sufficient — BPR may block push |
GH_PushProtectedBranch | No | Bypasses push-gate only — merge-gate may still block |
GH_BypassBranchProtection | No | Bypasses merge-gate only — push-gate may still block |
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:| Index | Key | Value | Purpose |
|---|---|---|---|
$nodeById | node ID | node object | Look up any node by ID |
$outbound | "edgeKind|startId" | list of end IDs | Follow edges forward |
$inbound | "edgeKind|endId" | list of start IDs | Follow edges backward |
| Index | Key | Value | Purpose |
|---|---|---|---|
$repoBranches | repo ID | list of branch IDs | Enumerate branches per repo |
$branchToBPR | branch ID | BPR ID | Find protecting rule for a branch |
$rolePermissions | role ID | HashSet of permission edge kinds | Direct permissions per role |
$pushAllowanceActors | BPR ID | HashSet of actor IDs | Actors with push allowance per rule |
$bypassPRActors | BPR ID | HashSet of actor IDs | Actors with PR bypass per rule |
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:
| Role | Direct Permissions | Inherited Permissions | Full Permission Set |
|---|---|---|---|
repoAdmin | {GH_AdminTo, GH_PushProtectedBranch, GH_BypassBranchProtection} | (none) | {GH_AdminTo, GH_PushProtectedBranch, GH_BypassBranchProtection} |
repoMaintain | {GH_PushProtectedBranch} | {GH_WriteRepoContents} (from write via HasBaseRole) | {GH_PushProtectedBranch, GH_WriteRepoContents} |
repoWrite | {GH_WriteRepoContents} | (none) | {GH_WriteRepoContents} |
| Custom role (base=write) | {GH_BypassBranchProtection} | {GH_WriteRepoContents} (from write via HasBaseRole) | {GH_BypassBranchProtection, GH_WriteRepoContents} |
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
Compute-GitHoundSecretScanningAccess is a post-collection step that computes effective secret scanning alert read access. It runs as Step 11.5 in Invoke-GitHound, after secret scanning alerts have been collected and before app installations are collected.
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
| Edge Kind | Source | Target | Traversable | Description |
|---|---|---|---|---|
GH_CanReadSecretScanningAlert | GH_OrgRole | GH_SecretScanningAlert | Yes | Org role can read all alerts in the organization |
GH_CanReadSecretScanningAlert | GH_RepoRole | GH_SecretScanningAlert | Yes | Repo role can read alerts in the repository |
Reason Values
| Reason | Meaning |
|---|---|
org_role_permission | Org role has GH_ViewSecretScanningAlerts on the organization containing the alert |
repo_role_permission | Repo role has GH_ViewSecretScanningAlerts on the repository containing the alert |
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: