> ## Documentation Index
> Fetch the complete documentation index at: https://bloodhound.specterops.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Computed Edges

> How OpenHound GitHub and GitHound compute effective branch access and secret scanning alert access edges

<img noZoom src="https://mintcdn.com/specterops/tTIczgde9H07oLXf/assets/enterprise-AND-community-edition-pill-tag.svg?fit=max&auto=format&n=tTIczgde9H07oLXf&q=85&s=ad49a576589f4d2a8081df77d07fdf56" alt="Applies to BloodHound Enterprise and CE" width="482" height="45" data-path="assets/enterprise-AND-community-edition-pill-tag.svg" />

This document describes the computed edge logic used by both the [OpenHound GitHub collector](/openhound/collectors/github/overview) and [GitHound](https://github.com/SpecterOps/GitHound), and the edges that logic produces. For the empirical testing that validates the underlying security model, see [Mitigating Controls](/opengraph/extensions/github/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:

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

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`:

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
```
