Documentation Index
Fetch the complete documentation index at: https://specterops-enable-tls-feedback.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
This document describes the computed edge logic used by both the OpenHound GitHub collector and GitHound, and the edges that logic produces. For the empirical testing that validates the underlying security model, see Mitigating Controls.
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 by Compute-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
| 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 |
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:
| 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 a query_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 when required_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 when push_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 |
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 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 |
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:
| 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 |
Domain-specific indexes:
| 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 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:
| 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 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:
- Look up the protecting BPR (if any)
- Evaluate the merge gate — blocked unless bypassed by admin or
bypass_branch_protection (both suppressed by enforce_admins)
- Evaluate the push gate — blocked unless bypassed by admin or
push_protected_branch (neither affected by enforce_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 in pushAllowances for the wildcard BPR. If so, emit actor → 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):
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
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 by Compute-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
| 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 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:
- Get the source role ID and target org ID
- Look up all alerts in that org
- 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:
- Get the source role ID and target repo ID
- Look up all alerts in that repo
- 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