Skip to main content
Applies to BloodHound Enterprise and CE This document describes the computed edge functions and what they produce. For the empirical testing that validates the underlying security model, see Mitigating Controls.

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 KindSourceTargetTraversableDescription
GH_CanCreateBranchGH_RepoRoleGH_RepositoryYesRole can create new branches
GH_CanCreateBranchGH_User or GH_TeamGH_RepositoryYesPer-rule allowance delta — actor can create branches when role alone doesn’t grant access
GH_CanWriteBranchGH_RepoRoleGH_BranchYesRole can push to this specific branch
GH_CanWriteBranchGH_User or GH_TeamGH_BranchYesPer-rule allowance delta — actor can push when role alone doesn’t grant access
GH_CanEditProtectionGH_RepoRoleGH_BranchYesRole can modify/remove the BPR(s) governing this branch
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 a reason property explaining why access was granted:
ReasonMeaning
no_protectionNo branch protection rule applies to this branch
adminAdmin access bypasses the gate
push_protected_branchRole has push_protected_branch permission (bypasses push gate)
bypass_branch_protectionRole has bypass_branch_protection permission (bypasses merge gate)
push_allowanceActor is in pushAllowances for the matching BPR
bypass_pr_allowanceActor is in bypassPullRequestAllowances (bypasses PR reviews only)
edit_repo_protectionsRole can modify/remove this BPR (used on GH_CanEditProtection edges)

Composition Queries

Each computed edge includes a query_composition property containing a Cypher query that reveals the underlying graph elements that caused the edge to be created.
Edge TypeSourceWhat the query shows
GH_CanWriteBranchRepoRole → BranchRole’s permission edges + BPR protecting the branch
GH_CanCreateBranchRepoRole → RepositoryRole’s permission edges + wildcard BPR (if any)
GH_CanEditProtectionRepoRole → BranchRole’s edit/admin permission edge + repo’s branches + protecting BPR(s)
GH_CanWriteBranchUser/Team → BranchActor’s allowance edges to the BPR + actor’s role path with permissions
GH_CanCreateBranchUser/Team → RepositoryActor’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 when required_pull_request_reviews or lock_branch is true on the protecting BPR.
Bypass MechanismScopeSuppressed by enforce_admins?
GH_AdminTo (admin access)Role-levelYes
GH_BypassBranchProtectionRole-levelYes
bypassPullRequestAllowancesPer-actorYes (PR reviews only, does not bypass lock_branch)

Push Gate

Active when push_restrictions is true on the protecting BPR.
Bypass MechanismScopeSuppressed by enforce_admins?
GH_AdminTo (admin access)Role-levelNo
GH_PushProtectedBranchRole-levelNo
pushAllowancesPer-actorNo
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:
Raw EdgeTraversableWhy not traversable
GH_WriteRepoContentsNoNecessary but not sufficient — BPR may block push
GH_PushProtectedBranchNoBypasses push-gate only — merge-gate may still block
GH_BypassBranchProtectionNoBypasses merge-gate only — push-gate may still block
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:
IndexKeyValuePurpose
$nodeByIdnode IDnode objectLook up any node by ID
$outbound"edgeKind|startId"list of end IDsFollow edges forward
$inbound"edgeKind|endId"list of start IDsFollow edges backward
Domain-specific indexes:
IndexKeyValuePurpose
$repoBranchesrepo IDlist of branch IDsEnumerate branches per repo
$branchToBPRbranch IDBPR IDFind protecting rule for a branch
$rolePermissionsrole IDHashSet of permission edge kindsDirect permissions per role
$pushAllowanceActorsBPR IDHashSet of actor IDsActors with push allowance per rule
$bypassPRActorsBPR IDHashSet of actor IDsActors with PR bypass per rule

Phase 2: Role Permission Resolution

Builds full permission sets for all roles by traversing the GH_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:
RoleDirect PermissionsInherited PermissionsFull 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 has GH_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 → emit role → repo (reason: push_protected_branch)
  • Otherwise → no edge
GH_CanWriteBranch: Evaluates the merge gate and push gate for each branch using only the role’s permissions:
  1. Look up the protecting BPR (if any)
  2. Evaluate the merge gate — blocked unless bypassed by admin or bypass_branch_protection (both suppressed by enforce_admins)
  3. Evaluate the push gate — blocked unless bypassed by admin or push_protected_branch (neither affected by enforce_admins)
  4. 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:
  1. Compute covered branches: Union of role-accessible branches across all leaf roles the actor reaches
  2. 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
  3. GH_CanCreateBranch delta: If a wildcard blocking BPR exists and the actor’s role doesn’t grant GH_CanCreateBranch, check if the actor is in pushAllowances for the wildcard BPR. If so, emit actor → repo (reason: push_allowance)
  4. 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):
User → GH_HasRole → RepoRole → GH_CanWriteBranch → Branch
User → GH_HasRole → OrgRole → GH_HasBaseRole → ... → RepoRole → GH_CanWriteBranch → Branch
Per-actor allowance delta:
User → GH_CanWriteBranch → Branch
Team → GH_CanWriteBranch → Branch

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 KindSourceTargetTraversableDescription
GH_CanReadSecretScanningAlertGH_OrgRoleGH_SecretScanningAlertYesOrg role can read all alerts in the organization
GH_CanReadSecretScanningAlertGH_RepoRoleGH_SecretScanningAlertYesRepo role can read alerts in the repository

Reason Values

ReasonMeaning
org_role_permissionOrg role has GH_ViewSecretScanningAlerts on the organization containing the alert
repo_role_permissionRepo 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 all GH_SecretScanningAlert node IDs
  • $orgAlerts — maps each org ID to its contained alert IDs
  • $repoAlerts — maps each repo ID to its contained alert IDs
  • GH_ViewSecretScanningAlerts edges are split into $orgViewEdges (target is GH_Organization) and $repoViewEdges (target is GH_Repository)

Phase 2: Org-Level Emission

For each GH_ViewSecretScanningAlerts edge targeting a GH_Organization:
  1. Get the source role ID and target org ID
  2. Look up all alerts in that org
  3. For each alert: emit GH_CanReadSecretScanningAlert from the org role to the alert (reason: org_role_permission)

Phase 3: Repo-Level Emission

For each GH_ViewSecretScanningAlerts edge targeting a GH_Repository:
  1. Get the source role ID and target repo ID
  2. Look up all alerts in that repo
  3. For each alert: emit GH_CanReadSecretScanningAlert from 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 the GH_ValidToken edge), the actor can impersonate the token owner and exercise all permissions granted to that token. Complete attack path:
User → GH_HasRole → OrgRole → GH_CanReadSecretScanningAlert → SecretScanningAlert → GH_ValidToken → CompromisedUser