Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion descriptions/edges/GH_AssignedTo.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## General Information

The non-traversable GH_AssignedTo edge represents an enterprise-scoped object being assigned to an organization. It is currently used for enterprise teams and enterprise runner groups. For runner groups, this edge indicates that an organization is allowed to inherit and use the enterprise-owned runner group.
The non-traversable GH_AssignedTo edge represents an enterprise-scoped team being assigned to an organization.

This edge is not traversable because assignment alone does not directly grant a principal a privilege path.
8 changes: 4 additions & 4 deletions descriptions/edges/GH_CanCreateRepositoryWithRunnerAccess.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
## General Information

The non-traversable GH_CanCreateRepositoryWithRunnerAccess edge is a computed edge indicating that a GH_OrgRole can create a repository whose visibility automatically places it in scope for a GH_OrgRunnerGroup.
The traversable GH_CanCreateRepositoryWithRunnerAccess edge is a computed edge indicating that a GH_OrgRole can create a repository that will immediately be able to dispatch workflows to a GH_OrgRunnerGroup.

This edge is emitted only for runner groups with `visibility=all` or `visibility=private`. Groups with `visibility=selected` require explicit repository assignment, so creating a repository does not automatically grant access to the group.
This edge is emitted only for runner groups with `visibility=all` or `visibility=private`. Groups with `visibility=selected` require explicit repository assignment, so creating a repository does not automatically grant access to the group. For `visibility=all`, public repository creation is included only when `allows_public_repositories=true`; otherwise the composition is limited to private and internal repository creation.

The computation follows repository-creation capability edges from the org role to the organization and then GH_Contains to the runner group. For `visibility=all`, public repository creation is included only when `allows_public_repositories=true`; otherwise the composition is limited to private and internal repository creation. Each edge includes a `query_composition` Cypher query showing the underlying graph evidence.
The collector emits this edge only when new repositories in the organization have GitHub Actions enabled by default (`actions_enabled_repositories=all`), the organization-facing runner group has `restricted_to_workflows=false`, and inherited enterprise-backed access also has `restricted_to_workflows=false` on the source GH_EnterpriseRunnerGroup.

This edge represents latent repository eligibility subject to runner-group workflow policy. It is non-traversable because creating an eligible repository does not by itself prove that arbitrary jobs can dispatch to the group's runners.
The computation follows repository-creation capability edges from the org role to the organization and then GH_Contains to the runner group. Each edge includes a `query_composition` Cypher query showing the repository-creation path and the Actions and runner-group policy predicates that make the path immediately usable.
6 changes: 4 additions & 2 deletions descriptions/edges/GH_CanUseRunner.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## General Information

The non-traversable GH_CanUseRunner edge represents that a repository can dispatch GitHub Actions jobs to a self-hosted runner. Repository-scoped runners receive this edge directly from their containing repository. Organization and enterprise runners receive composed GH_CanUseRunner edges derived from runner group containment and repository access policy.
For runner-group-backed access, the traversable GH_CanUseRunner edge is a computed edge representing that a repository or branch can dispatch workflows to a self-hosted runner execution surface under the modeled runner-group controls.

For native organization runner groups, the composition path is `GH_Repository <- GH_GrantsAccessTo - GH_OrgRunnerGroup - GH_Contains -> GH_OrgRunner`. For inherited groups, the path continues through `GH_InheritedFrom` to the enterprise runner group and its contained GH_EnterpriseRunner nodes.
The collector derives this edge from GH_IsEligibleFor rather than directly from repository visibility. It emits GH_CanUseRunner only when the repository is within the runner group's repository-access scope, GitHub Actions is enabled for the repository, and `restricted_to_workflows=false` on the organization-facing runner group. Inherited enterprise-backed access also requires `restricted_to_workflows=false` on the source GH_EnterpriseRunnerGroup. Every collected branch in a repository that satisfies those conditions receives the same edge so branch write paths can reach the execution surface.

Organization and inherited enterprise-backed access terminate at the organization-facing GH_OrgRunnerGroup, then continue through GH_HasRunner for native organization runners or through GH_InheritedFrom and GH_HasRunner for inherited enterprise runners. Repository-scoped runners currently receive GH_CanUseRunner directly from their containing repository and are not part of this runner-group traversability change.
5 changes: 0 additions & 5 deletions descriptions/edges/GH_GrantsAccessTo.md

This file was deleted.

5 changes: 5 additions & 0 deletions descriptions/edges/GH_HasRunner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## General Information

The traversable GH_HasRunner edge represents that a runner group exposes a directly assigned self-hosted runner to repositories or workflows that satisfy the runner group's access policy.

This edge is distinct from GH_Contains. GH_Contains records structural membership only, while GH_HasRunner represents the runner-group-to-runner capability hop used for attack-path composition. This edge is emitted only for direct organization and enterprise runner group memberships; inherited organization runner group access to enterprise runners is modeled as GH_Repository or GH_Branch -[:GH_CanUseRunner]-> GH_OrgRunnerGroup -[:GH_InheritedFrom]-> GH_EnterpriseRunnerGroup -[:GH_HasRunner]-> GH_EnterpriseRunner.
4 changes: 2 additions & 2 deletions descriptions/edges/GH_InheritedFrom.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## General Information

The non-traversable GH_InheritedFrom edge links an inherited GH_OrgRunnerGroup to the GH_EnterpriseRunnerGroup that owns the underlying runner set. This preserves the organization-local view of a runner group while still identifying the enterprise source that provides the runners.
The traversable GH_InheritedFrom edge links an inherited GH_OrgRunnerGroup to the GH_EnterpriseRunnerGroup that owns the underlying runner set. This preserves the organization-local view of a runner group while still identifying the enterprise source that provides the runners.

This edge is not traversable because inheritance describes resource provenance rather than a direct privilege path.
This edge is traversable because an inherited organization runner group is the organization-facing policy boundary for the enterprise runner group. Repository access flows through the organization runner group, then through GH_InheritedFrom to the enterprise group, and finally through GH_HasRunner to directly assigned enterprise runners.
5 changes: 5 additions & 0 deletions descriptions/edges/GH_IsEligibleFor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## General Information

The non-traversable GH_IsEligibleFor edge represents that a repository is within the repository-access scope of an organization runner group.

For runner groups, this edge evaluates the group's `visibility`, selected repository assignments, and `allows_public_repositories` setting. It does not prove that workflows in the repository can dispatch to the group's runners, because GitHub Actions may be disabled for the repository or the runner group may be restricted to selected workflows.
2 changes: 1 addition & 1 deletion descriptions/nodes/GH_EnterpriseRunner.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## Description

Represents a self-hosted runner owned at the GitHub Enterprise level. Enterprise runners are contained by GH_EnterpriseRunnerGroup nodes and become usable by repositories when an inherited organization runner group grants access to those repositories.
Represents a self-hosted runner owned at the GitHub Enterprise level. Enterprise runners are contained by GH_EnterpriseRunnerGroup nodes and exposed through GH_HasRunner. Repositories become eligible for the organization-facing runner group through GH_IsEligibleFor. Repositories and branches that can dispatch workflows then reach the runner through GH_CanUseRunner to an inherited GH_OrgRunnerGroup, GH_InheritedFrom to the enterprise group, and finally GH_HasRunner to the runner.

The node captures runner metadata such as operating system, status, busy state, labels, and whether the runner is ephemeral when GitHub returns that property.
4 changes: 2 additions & 2 deletions descriptions/nodes/GH_EnterpriseRunnerGroup.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## Description

Represents a self-hosted runner group owned by a GitHub Enterprise account. Enterprise runner groups control which organizations may use a shared set of enterprise runners. Groups with `visibility=all` are assigned to every organization in the enterprise, while groups with `visibility=selected` are assigned only to explicitly selected organizations.
Represents a self-hosted runner group owned by a GitHub Enterprise account. Enterprise runner groups control which organizations may use a shared set of enterprise runners. That organization-level visibility is reflected by which inherited GH_OrgRunnerGroup nodes point back to the enterprise group through GH_InheritedFrom.

Enterprise runner groups contain GH_EnterpriseRunner nodes and may be projected into organizations as inherited GH_OrgRunnerGroup nodes. The GH_InheritedFrom edge links the organization view back to the enterprise-owned group.
Enterprise runner groups contain GH_EnterpriseRunner nodes and emit GH_HasRunner for directly assigned runners to represent the traversable capability hop from the group to the runner. They may be projected into organizations as inherited GH_OrgRunnerGroup nodes. The GH_InheritedFrom edge links the organization view back to the enterprise-owned group.
2 changes: 1 addition & 1 deletion descriptions/nodes/GH_OrgRunner.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## Description

Represents a self-hosted runner owned by a GitHub organization. Organization runners are contained by native GH_OrgRunnerGroup nodes and become usable by repositories through the runner group's repository access policy.
Represents a self-hosted runner owned by a GitHub organization. Organization runners are contained by native GH_OrgRunnerGroup nodes and exposed through GH_HasRunner. Repositories become eligible for those groups through GH_IsEligibleFor, while repositories and branches that can dispatch workflows to them are linked through GH_CanUseRunner.

The node captures runner metadata such as operating system, status, busy state, labels, and whether the runner is ephemeral when GitHub returns that property.
2 changes: 1 addition & 1 deletion descriptions/nodes/GH_OrgRunnerGroup.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

Represents a self-hosted runner group visible within a GitHub organization. Organization runner groups may either be native to the organization or inherited from an enterprise runner group.

Native organization runner groups contain GH_OrgRunner nodes directly. Inherited organization runner groups do not directly contain organization runners; instead, they link to the source GH_EnterpriseRunnerGroup through GH_InheritedFrom and gain access to the enterprise runners contained there. GH_GrantsAccessTo edges describe which repositories may use the runners exposed by the group.
Native organization runner groups contain GH_OrgRunner nodes directly. Direct memberships also emit GH_HasRunner to represent the traversable capability hop from the group to its runners. Inherited organization runner groups do not directly contain organization runners; instead, they link to the source GH_EnterpriseRunnerGroup through GH_InheritedFrom and gain access to the enterprise runners contained there. GH_IsEligibleFor edges from repositories describe repository access policy scope, while GH_CanUseRunner edges identify repositories and branches that can dispatch workflows to the group under the collected Actions and workflow-restriction settings.
21 changes: 13 additions & 8 deletions extension/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -442,18 +442,18 @@
},
{
"name": "GH_AssignedTo",
"description": "Enterprise-scoped team or runner group is assigned to an organization",
"description": "Enterprise-scoped team is assigned to an organization",
"is_traversable": false
},
{
"name": "GH_InheritedFrom",
"description": "Organization runner group is inherited from an enterprise runner group",
"is_traversable": false
"is_traversable": true
},
{
"name": "GH_GrantsAccessTo",
"description": "Organization runner group grants a repository access to its available runners",
"is_traversable": false
"name": "GH_HasRunner",
"description": "Runner group exposes a directly assigned self-hosted runner to authorized repositories or workflows",
"is_traversable": true
},
{
"name": "GH_Owns",
Expand Down Expand Up @@ -967,13 +967,18 @@
},
{
"name": "GH_CanUseRunner",
"description": "Repository can dispatch jobs to this self-hosted runner",
"description": "Repository or branch can dispatch workflows to this self-hosted runner execution surface",
"is_traversable": true
},
{
"name": "GH_IsEligibleFor",
"description": "Repository is within the repository-access scope of this runner group",
"is_traversable": false
},
{
"name": "GH_CanCreateRepositoryWithRunnerAccess",
"description": "Org role can create a repository that inherits access to this runner group",
"is_traversable": false
"description": "Org role can create a repository that can dispatch workflows to this runner group",
"is_traversable": true
},
{
"name": "GH_CanWriteBranch",
Expand Down
3 changes: 2 additions & 1 deletion src/openhound_github/kinds/edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
CONTAINS = "GH_Contains"
ASSIGNED_TO = "GH_AssignedTo"
INHERITED_FROM = "GH_InheritedFrom"
GRANTS_ACCESS_TO = "GH_GrantsAccessTo"

# Administrative edges
ADMIN_TO = "GH_AdminTo"
Expand All @@ -12,12 +11,14 @@
HAS_MEMBER = "GH_HasMember"
HAS_ROLE = "GH_HasRole"
HAS_BASE_ROLE = "GH_HasBaseRole"
HAS_RUNNER = "GH_HasRunner"
ADD_MEMBER = "GH_AddMember"
MEMBER_OF = "GH_MemberOf"

# Access and capability edges
CAN_ACCESS = "GH_CanAccess"
CAN_USE_RUNNER = "GH_CanUseRunner"
IS_ELIGIBLE_FOR = "GH_IsEligibleFor"
CAN_CREATE_REPOSITORY_WITH_RUNNER_ACCESS = "GH_CanCreateRepositoryWithRunnerAccess"
CAN_CREATE_BRANCH = "GH_CanCreateBranch"
CAN_CREATE_ENVIRONMENT = "GH_CanCreateEnvironment"
Expand Down
51 changes: 51 additions & 0 deletions src/openhound_github/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,30 @@ def enterprise_runner_group_node_id_for_inherited_org_group(
enterprise_node_id, runner_group_id = identity
return runner_group_node_id(enterprise_node_id, runner_group_id)

@lru_cache
def enterprise_runner_group_restricted_to_workflows_for_inherited_org_group(
self, org_node_id: str, group_name: str
) -> bool | None:
identity = self._enterprise_runner_group_identity_for_inherited_org_group(
org_node_id, group_name
)
if not identity:
return None

enterprise_node_id, runner_group_id = identity
row = self._find_single_row(
f"""
SELECT restricted_to_workflows
FROM {self.schema}.enterprise_runner_groups
WHERE enterprise_node_id = ?
AND id = ?
""",
[enterprise_node_id, runner_group_id],
)
if row is None or row[0] is None:
return None
return bool(row[0])

@lru_cache
def enterprise_runner_node_ids_for_inherited_org_group(
self, org_node_id: str, group_name: str
Expand Down Expand Up @@ -195,6 +219,33 @@ def private_repository_node_ids_for_org(self, org_login: str):
[org_login],
)

@lru_cache
def actions_enabled_repository_node_ids_for_org(self, org_login: str):
return self._find_all_objects(
f"""SELECT node_id FROM {self.schema}.repositories WHERE org_login = ? AND actions_enabled = true""",
[org_login],
)

@lru_cache
def actions_enabled_repositories_for_org(self, org_login: str) -> str | None:
return self._find_single_object(
f"""SELECT actions_enabled_repositories FROM {self.schema}.organizations WHERE login = ?""",
[org_login],
)

@lru_cache
def branch_node_ids_for_org(self, org_login: str):
return self._find_all_objects(
f"""
SELECT b.repository_node_id, b.id
FROM {self.schema}.branches b
JOIN {self.schema}.repositories r
ON r.node_id = b.repository_node_id
WHERE r.org_login = ?
""",
[org_login],
)

@lru_cache
def repository_branch_ruleset_count(self, repository_node_id: str) -> int | None:
row = self._find_single_row(
Expand Down
8 changes: 7 additions & 1 deletion src/openhound_github/models/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,13 @@ def as_node(self) -> GHNode:
query_roles=f"MATCH p=(:GH_RepoRole)-[*1..]->(:GH_Repository {{node_id: '{rid}'}}) RETURN p",
query_teams=f"MATCH p=(:GH_Team)-[:GH_MemberOf|GH_HasRole*1..]->(:GH_RepoRole)-[]->(:GH_Repository {{node_id: '{rid}'}}) RETURN p",
query_workflows=f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_Contains]->(:GH_Workflow)-[:GH_Contains]->(:GH_WorkflowJob)-[:GH_Contains]->(step:GH_WorkflowStep) OPTIONAL MATCH p1=(step)-[:GH_UsesSecret]->(:GH_Secret) OPTIONAL MATCH p2=(step)-[:GH_UsesVariable]->(:GH_Variable) RETURN p,p1,p2",
query_runners=f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_CanUseRunner]->(:GH_Runner) RETURN p",
query_runners=(
f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_CanUseRunner]->(:GH_RepoRunner) RETURN p "
"UNION "
f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_CanUseRunner]->(:GH_OrgRunnerGroup)-[:GH_HasRunner]->(:GH_OrgRunner) RETURN p "
"UNION "
f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_CanUseRunner]->(:GH_OrgRunnerGroup)-[:GH_InheritedFrom]->(:GH_EnterpriseRunnerGroup)-[:GH_HasRunner]->(:GH_EnterpriseRunner) RETURN p"
),
query_environments=f"MATCH p=(:GH_Repository {{node_id: '{rid}'}})-[:GH_Contains]->(:GH_Environment) RETURN p",
query_secrets=f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_HasSecret]->(:GH_Secret) RETURN p",
query_variables=f"MATCH p=(:GH_Repository {{node_id:'{rid}'}})-[:GH_HasVariable]->(:GH_Variable) RETURN p",
Expand Down
Loading
Loading