Skip to content
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,49 @@ The collector runs successfully with the documented read-only permission set. So

If these optional permissions are not granted, OpenHound skips the affected resources and continues collecting the rest of the GitHub environment.

### Classic personal access token inventory

With GitHub Enterprise Cloud credentials, the collector uses the enterprise
credential inventory export to collect classic personal access tokens. It
requests the full inventory, stores every CSV row and its original field values
in the `enterprise_credential_inventory` resource, and currently models only
classic PAT rows. The export contains credential metadata, including owners,
scopes, authorization details, and token hashes used for audit correlation. It
does not contain plaintext PAT values. Limit access to the raw output to people
who need the credential inventory.

The enterprise GitHub App needs **Enterprise credentials: read** permission.
When collecting with a classic PAT instead of an app installation, that token
needs the `read:enterprise` scope. If an enterprise app also has
`credentials.pat_token` configured, the app remains the primary export
credential. The collector retries once with the PAT only when GitHub rejects
the app's export creation for authorization. It does not switch credentials
after an export starts or when GitHub reports a rate limit.

Rate-limit retry warnings identify the affected method and API path.

The CSV download is streamed, but the parsed export is held in memory as one
raw record; very large enterprises may need a chunked raw-resource design.

GitHub limits the number of exports an enterprise can start per day. When the
last successful export was downloaded within the past 24 hours, the collector
downloads it again before trying to start a new export. If GitHub reports that
the saved export is gone, the collector starts a new one and polls until its
CSV is ready. GitHub may still reject a new export when its limit is reached.
The raw record's `as_of` value and each PAT node's `inventory_as_of` value
show when that snapshot was taken. An unavailable or denied export is logged
and does not stop other resources from collecting.

Classic PAT nodes include the owner, scopes, lifecycle dates, credential
state, direct enterprise authorization, and total authorization count.
Organization edges record authorizations reported in the export. The
export does not enumerate repositories accessible to a classic PAT, so this
collection does not emit classic PAT-to-repository access edges. Potential
repository access can be derived from the token owner's repository roles and
the token's scopes, subject to organization policy and SSO authorization; it
is not a direct grant reported by the export. GitHub Enterprise Server and
organization-only configurations skip this resource.

[![Python Version](https://img.shields.io/badge/Python-3.13-brightgreen.svg)](#about)

## Getting Started
Expand Down
20 changes: 20 additions & 0 deletions descriptions/edges/GH_AuthorizedForOrganization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# GH_AuthorizedForOrganization

## General Information

Classic personal access token has a recorded authorization for this organization.

## Edge Schema

| Source | Destination | Traversable |
| --- | --- | --- |
| `GH_ClassicPersonalAccessToken` | `GH_Organization` | `false` |

## Diagram

```mermaid
graph LR
n0["GH_ClassicPersonalAccessToken"]
n1["GH_Organization"]
n0 -.->|GH_AuthorizedForOrganization| n1
```
107 changes: 55 additions & 52 deletions descriptions/edges/GH_Contains.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

## General Information

The non-traversable GH_Contains edge represents structural containment within the GitHub resource hierarchy. The enterprise contains enterprise teams, roles, managed users, runner groups, and enterprise runners through their groups. The organization serves as a top-level container for users, teams, repositories, roles, secrets, app installations, personal access tokens, and organization runner groups. Native organization runner groups contain organization runners. Repositories contain branches, workflows, branch protection rules, environments, repo-level secrets and variables, and repository-scoped runners. Environments contain environment branch policies, environment-scoped secrets, and environment-scoped variables. This edge is created by the collector to establish the resource hierarchy and is not traversable because containment alone does not imply privilege escalation.
The non-traversable GH_Contains edge represents structural containment within the GitHub resource hierarchy. The enterprise contains enterprise teams, roles, managed users, classic personal access tokens, runner groups, and enterprise runners through their groups. The organization serves as a top-level container for users, teams, repositories, roles, secrets, app installations, fine-grained personal access tokens, and organization runner groups. Native organization runner groups contain organization runners. Repositories contain branches, workflows, branch protection rules, environments, repo-level secrets and variables, and repository-scoped runners. Environments contain environment branch policies, environment-scoped secrets, and environment-scoped variables. This edge is created by the collector to establish the resource hierarchy and is not traversable because containment alone does not imply privilege escalation.

## Edge Schema

| Source | Destination | Traversable |
| --- | --- | --- |
| `GH_Enterprise` | `GH_ClassicPersonalAccessToken` | `false` |
| `GH_Enterprise` | `GH_EnterpriseRole` | `false` |
| `GH_Enterprise` | `GH_EnterpriseRunnerGroup` | `false` |
| `GH_Enterprise` | `GH_EnterpriseTeam` | `false` |
Expand Down Expand Up @@ -42,60 +43,62 @@ The non-traversable GH_Contains edge represents structural containment within th
```mermaid
graph LR
n0["GH_Enterprise"]
n1["GH_EnterpriseRole"]
n2["GH_EnterpriseRunnerGroup"]
n3["GH_EnterpriseTeam"]
n4["GH_Organization"]
n5["GH_EnterpriseRunner"]
n6["GH_Environment"]
n7["GH_EnvironmentBranchPolicy"]
n8["GH_EnvironmentSecret"]
n9["GH_EnvironmentVariable"]
n10["GH_OrgRunnerGroup"]
n11["GH_OrgRunner"]
n12["GH_AppInstallation"]
n13["GH_OrgRole"]
n14["GH_OrgSecret"]
n15["GH_OrgVariable"]
n16["GH_PersonalAccessToken"]
n17["GH_PersonalAccessTokenRequest"]
n18["GH_SecretScanningAlert"]
n19["GH_Repository"]
n20["GH_Branch"]
n21["GH_BranchProtectionRule"]
n22["GH_DeployKey"]
n23["GH_RepoRunner"]
n24["GH_RepoSecret"]
n25["GH_RepoVariable"]
n26["GH_Workflow"]
n27["GH_WorkflowJob"]
n28["GH_WorkflowStep"]
n1["GH_ClassicPersonalAccessToken"]
n2["GH_EnterpriseRole"]
n3["GH_EnterpriseRunnerGroup"]
n4["GH_EnterpriseTeam"]
n5["GH_Organization"]
n6["GH_EnterpriseRunner"]
n7["GH_Environment"]
n8["GH_EnvironmentBranchPolicy"]
n9["GH_EnvironmentSecret"]
n10["GH_EnvironmentVariable"]
n11["GH_OrgRunnerGroup"]
n12["GH_OrgRunner"]
n13["GH_AppInstallation"]
n14["GH_OrgRole"]
n15["GH_OrgSecret"]
n16["GH_OrgVariable"]
n17["GH_PersonalAccessToken"]
n18["GH_PersonalAccessTokenRequest"]
n19["GH_SecretScanningAlert"]
n20["GH_Repository"]
n21["GH_Branch"]
n22["GH_BranchProtectionRule"]
n23["GH_DeployKey"]
n24["GH_RepoRunner"]
n25["GH_RepoSecret"]
n26["GH_RepoVariable"]
n27["GH_Workflow"]
n28["GH_WorkflowJob"]
n29["GH_WorkflowStep"]
n0 -.->|GH_Contains| n1
n0 -.->|GH_Contains| n2
n0 -.->|GH_Contains| n3
n0 -.->|GH_Contains| n4
n2 -.->|GH_Contains| n5
n6 -.->|GH_Contains| n7
n6 -.->|GH_Contains| n8
n6 -.->|GH_Contains| n9
n10 -.->|GH_Contains| n11
n4 -.->|GH_Contains| n12
n4 -.->|GH_Contains| n13
n4 -.->|GH_Contains| n10
n4 -.->|GH_Contains| n14
n4 -.->|GH_Contains| n15
n4 -.->|GH_Contains| n16
n4 -.->|GH_Contains| n17
n4 -.->|GH_Contains| n18
n19 -.->|GH_Contains| n20
n19 -.->|GH_Contains| n21
n19 -.->|GH_Contains| n22
n19 -.->|GH_Contains| n6
n19 -.->|GH_Contains| n23
n19 -.->|GH_Contains| n24
n19 -.->|GH_Contains| n25
n19 -.->|GH_Contains| n18
n19 -.->|GH_Contains| n26
n26 -.->|GH_Contains| n27
n0 -.->|GH_Contains| n5
n3 -.->|GH_Contains| n6
n7 -.->|GH_Contains| n8
n7 -.->|GH_Contains| n9
n7 -.->|GH_Contains| n10
n11 -.->|GH_Contains| n12
n5 -.->|GH_Contains| n13
n5 -.->|GH_Contains| n14
n5 -.->|GH_Contains| n11
n5 -.->|GH_Contains| n15
n5 -.->|GH_Contains| n16
n5 -.->|GH_Contains| n17
n5 -.->|GH_Contains| n18
n5 -.->|GH_Contains| n19
n20 -.->|GH_Contains| n21
n20 -.->|GH_Contains| n22
n20 -.->|GH_Contains| n23
n20 -.->|GH_Contains| n7
n20 -.->|GH_Contains| n24
n20 -.->|GH_Contains| n25
n20 -.->|GH_Contains| n26
n20 -.->|GH_Contains| n19
n20 -.->|GH_Contains| n27
n27 -.->|GH_Contains| n28
n28 -.->|GH_Contains| n29
```
7 changes: 5 additions & 2 deletions descriptions/edges/GH_HasPersonalAccessToken.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@

## General Information

The non-traversable GH_HasPersonalAccessToken edge represents the relationship between a user and their fine-grained personal access tokens that have been granted access to the organization. This edge links each approved token back to the user who created it. Fine-grained personal access tokens are security-significant because they provide programmatic access to organization resources with specific scoped permissions. Tracking token ownership is essential for understanding which users have standing API access and for identifying tokens that may need revocation.
The non-traversable GH_HasPersonalAccessToken edge links a user to a personal access token they own. Fine-grained token ownership comes from organization approvals; classic token ownership comes from the enterprise credential inventory. The edge identifies the owner but does not by itself grant access to any organization or repository.

## Edge Schema

| Source | Destination | Traversable |
| --- | --- | --- |
| `GH_User` | `GH_ClassicPersonalAccessToken` | `false` |
| `GH_User` | `GH_PersonalAccessToken` | `false` |

## Diagram

```mermaid
graph LR
n0["GH_User"]
n1["GH_PersonalAccessToken"]
n1["GH_ClassicPersonalAccessToken"]
n2["GH_PersonalAccessToken"]
n0 -.->|GH_HasPersonalAccessToken| n1
n0 -.->|GH_HasPersonalAccessToken| n2
```
43 changes: 43 additions & 0 deletions descriptions/nodes/GH_ClassicPersonalAccessToken.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# GH_ClassicPersonalAccessToken

## General Information

A classic personal access token found in the enterprise credential inventory.

## Properties

| Property | Type | Description |
| --- | --- | --- |
| `name` | `string` | The node name used for matching and display. |
| `displayname` | `string` | The human-readable display name. |
| `environmentid` | `string` | The identifier of the GitHub environment where this node was collected. |
| `last_seen` | `datetime` | The timestamp when this node was last observed during collection. |
| `node_id` | `string` | The stable identifier used as the OpenGraph node ID; this is the native GitHub node ID where available. |
| `credential_id` | `integer` | GitHub's ID for this classic personal access token. |
| `owner_id` | `integer` | GitHub's numeric ID for the token owner. |
| `owner_login` | `string` | Login of the token owner. |
| `scopes` | `list[string]` | OAuth scopes granted to the token. |
| `credential_state` | `string` | Whether GitHub reports the credential as active, expired, revoked, or deleted. |
| `expiry_status` | `string` | Whether expiration is scheduled, never, or unknown. |
| `enterprise_authorized` | `boolean` | Whether the credential is authorized directly for the enterprise. |
| `authorization_count` | `integer` | Total organization authorizations plus enterprise authorization. |
| `inventory_as_of` | `string` | Timestamp of the export snapshot used to observe the token. |
| `created_at` | `datetime` | Token creation time reported by GitHub. |
| `last_used_at` | `datetime` | Last use time reported by GitHub. |
| `expires_at` | `datetime` | Token expiration time reported by GitHub. |
| `authorized_organizations` | `list[string]` | Organizations with a reported credential authorization. |
| `environment_name` | `string` | Enterprise environment name. |
| `enterprise_name` | `string` | Enterprise from which the inventory was exported. |

## Diagram

```mermaid
graph LR
n0["GH_ClassicPersonalAccessToken"]
n1["GH_Organization"]
n2["GH_Enterprise"]
n3["GH_User"]
n0 -.->|GH_AuthorizedForOrganization| n1
n2 -.->|GH_Contains| n0
n3 -.->|GH_HasPersonalAccessToken| n0
```
70 changes: 36 additions & 34 deletions descriptions/nodes/GH_Enterprise.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,41 +34,43 @@ A GitHub Enterprise account that contains organizations, enterprise teams, roles
```mermaid
graph LR
n0["GH_Enterprise"]
n1["GH_EnterpriseManagedUser"]
n2["GH_EnterpriseRole"]
n3["GH_EnterpriseRunnerGroup"]
n4["GH_EnterpriseTeam"]
n5["GH_Organization"]
n6["GH_SamlIdentityProvider"]
n7["GH_User"]
n0 -.->|GH_HasMember| n1
n0 -.->|GH_Contains| n2
n1["GH_ClassicPersonalAccessToken"]
n2["GH_EnterpriseManagedUser"]
n3["GH_EnterpriseRole"]
n4["GH_EnterpriseRunnerGroup"]
n5["GH_EnterpriseTeam"]
n6["GH_Organization"]
n7["GH_SamlIdentityProvider"]
n8["GH_User"]
n0 -.->|GH_Contains| n1
n0 -.->|GH_HasMember| n2
n0 -.->|GH_Contains| n3
n0 -.->|GH_Contains| n4
n0 -.->|GH_Contains| n5
n0 -.->|GH_HasSamlIdentityProvider| n6
n0 -.->|GH_HasMember| n7
n2 -.->|GH_CreateEnterpriseOrganizations| n0
n2 -.->|GH_EditEnterpriseCustomPropertiesForOrganizations| n0
n2 -->|GH_ManageEnterpriseAdmins| n0
n2 -.->|GH_ManageEnterpriseIdentityProvider| n0
n2 -->|GH_ManageEnterpriseMembers| n0
n2 -->|GH_ManageEnterpriseOrganizationAdmins| n0
n2 -.->|GH_ManageEnterpriseOrganizations| n0
n2 -.->|GH_ManageEnterpriseReferrals| n0
n2 -.->|GH_ManageEnterpriseTeams| n0
n2 -.->|GH_ReadEnterpriseAuditLog| n0
n2 -.->|GH_ReadEnterpriseDomainVerification| n0
n2 -.->|GH_ReadEnterpriseMembers| n0
n2 -.->|GH_ReadEnterpriseOrgProjects| n0
n2 -.->|GH_ReadEnterpriseOrganizationAdmin| n0
n2 -.->|GH_SetEnterpriseInteractionLimits| n0
n2 -.->|GH_ViewEnterpriseActionsUsageMetrics| n0
n2 -.->|GH_ViewEnterpriseBilling| n0
n2 -.->|GH_ViewEnterpriseSecretScanningAlerts| n0
n2 -.->|GH_WriteEnterpriseActionsPolicies| n0
n2 -.->|GH_WriteEnterpriseBilling| n0
n2 -.->|GH_WriteEnterprisePersonalAccessTokenPolicies| n0
n2 -.->|GH_WriteEnterpriseSso| n0
n2 -.->|GH_WriteEnterpriseTeamMembers| n0
n0 -.->|GH_Contains| n6
n0 -.->|GH_HasSamlIdentityProvider| n7
n0 -.->|GH_HasMember| n8
n3 -.->|GH_CreateEnterpriseOrganizations| n0
n3 -.->|GH_EditEnterpriseCustomPropertiesForOrganizations| n0
n3 -->|GH_ManageEnterpriseAdmins| n0
n3 -.->|GH_ManageEnterpriseIdentityProvider| n0
n3 -->|GH_ManageEnterpriseMembers| n0
n3 -->|GH_ManageEnterpriseOrganizationAdmins| n0
n3 -.->|GH_ManageEnterpriseOrganizations| n0
n3 -.->|GH_ManageEnterpriseReferrals| n0
n3 -.->|GH_ManageEnterpriseTeams| n0
n3 -.->|GH_ReadEnterpriseAuditLog| n0
n3 -.->|GH_ReadEnterpriseDomainVerification| n0
n3 -.->|GH_ReadEnterpriseMembers| n0
n3 -.->|GH_ReadEnterpriseOrgProjects| n0
n3 -.->|GH_ReadEnterpriseOrganizationAdmin| n0
n3 -.->|GH_SetEnterpriseInteractionLimits| n0
n3 -.->|GH_ViewEnterpriseActionsUsageMetrics| n0
n3 -.->|GH_ViewEnterpriseBilling| n0
n3 -.->|GH_ViewEnterpriseSecretScanningAlerts| n0
n3 -.->|GH_WriteEnterpriseActionsPolicies| n0
n3 -.->|GH_WriteEnterpriseBilling| n0
n3 -.->|GH_WriteEnterprisePersonalAccessTokenPolicies| n0
n3 -.->|GH_WriteEnterpriseSso| n0
n3 -.->|GH_WriteEnterpriseTeamMembers| n0
```
Loading
Loading