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_CanPwnRequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ An attacker who exploits a pwn request gains code execution in the workflow runn

### Caveats

- **OIDC traversal requires `id-token: write`**: The attack chain from GH_CanPwnRequest through GH_CanAssumeIdentity to a cloud role is only valid if the pwn-requestable workflow (or job) explicitly declares `id-token: write` in its `permissions:` block. The `id-token` permission defaults to `none` and is never implicitly granted — even when the workflow has no `permissions:` block at all. The `permissions` property on the GH_WorkflowJob node can be inspected to verify this.
- **OIDC traversal requires `id-token: write`**: The attack chain from GH_CanPwnRequest through GH_CanAssumeIdentity to a cloud role is only valid if the pwn-requestable job's calculated `effective_github_token_permissions` includes `id-token:write`. The `id-token` permission defaults to `none` and is never implicitly granted — even when the workflow has no `permissions:` block at all. Inspect `workflow_permissions` on GH_Workflow, `job_permissions` on GH_WorkflowJob, and the job's `effective_github_token_permissions` to understand how the effective value was derived.
- **GITHUB_TOKEN permissions**: The `permissions:` block controls what the `GITHUB_TOKEN` can do (e.g., push commits, create releases), but has no effect on secret access, OIDC token requests (governed separately by `id-token`), or arbitrary code execution. A workflow with `contents: read` is still fully exploitable via pwn request for secret exfiltration and lateral movement — only write-back to the repository is limited.

## Edge Schema
Expand Down
24 changes: 24 additions & 0 deletions descriptions/edges/GH_RunsOn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# GH_RunsOn

## General Information

The non-traversable GH_RunsOn edge represents that a GitHub Actions workflow job can be scheduled on a self-hosted runner based on the job's statically declared `runs-on` selector and the runner topology visible to the containing repository.

This edge is schedulability evidence, not historical execution evidence. It does not mean that the job has previously executed on the runner. It means that the runner satisfies the job's static label and runner-group requirements and is reachable through the repository's current runner access policy.

The collector emits GH_RunsOn only for static selectors. Dynamic selectors that contain GitHub Actions expressions such as `${{ matrix.runner }}` or `${{ inputs.runner }}` are intentionally left unresolved in this first implementation.

## Edge Schema

| Source | Destination | Traversable |
| --- | --- | --- |
| `GH_WorkflowJob` | `GH_Runner` | `false` |

## Diagram

```mermaid
graph LR
n0["GH_WorkflowJob"]
n1["GH_Runner"]
n0 -.->|GH_RunsOn| n1
```
5 changes: 5 additions & 0 deletions descriptions/nodes/GH_EnterpriseRunner.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Represents a self-hosted runner owned at the GitHub Enterprise level. Enterprise

The node captures runner metadata such as operating system, status, busy state, labels, and whether the runner is ephemeral when GitHub returns that property.

GH_RunsOn edges from GH_WorkflowJob nodes identify statically resolvable jobs that GitHub could schedule on this runner through the inherited enterprise runner-group topology. These edges do not indicate that the job has actually executed on the runner.

## Properties

| Property | Type | Description |
Expand All @@ -31,13 +33,16 @@ The node captures runner metadata such as operating system, status, busy state,
| `environment_name` | `string` | The name of the environment (GitHub organization). |
| `query_group` | `string` | Query for group. |
| `query_repositories` | `string` | Query for repositories. |
| `query_jobs` | `string` | Query for workflow jobs that can be scheduled on the runner. |

## Diagram

```mermaid
graph LR
n0["GH_EnterpriseRunnerGroup"]
n1["GH_EnterpriseRunner"]
n2["GH_WorkflowJob"]
n0 -.->|GH_Contains| n1
n0 -->|GH_HasRunner| n1
n2 -.->|GH_RunsOn| n1
```
5 changes: 5 additions & 0 deletions descriptions/nodes/GH_OrgRunner.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Represents a self-hosted runner owned by a GitHub organization. Organization run

The node captures runner metadata such as operating system, status, busy state, labels, and whether the runner is ephemeral when GitHub returns that property.

GH_RunsOn edges from GH_WorkflowJob nodes identify statically resolvable jobs that GitHub could schedule on this runner under the current runner-group access policy. These edges do not indicate that the job has actually executed on the runner.

## Properties

| Property | Type | Description |
Expand All @@ -31,13 +33,16 @@ The node captures runner metadata such as operating system, status, busy state,
| `environment_name` | `string` | The name of the environment (GitHub organization). |
| `query_group` | `string` | Query for group. |
| `query_repositories` | `string` | Query for repositories. |
| `query_jobs` | `string` | Query for workflow jobs that can be scheduled on the runner. |

## Diagram

```mermaid
graph LR
n0["GH_OrgRunnerGroup"]
n1["GH_OrgRunner"]
n2["GH_WorkflowJob"]
n0 -.->|GH_Contains| n1
n0 -->|GH_HasRunner| n1
n2 -.->|GH_RunsOn| n1
```
5 changes: 5 additions & 0 deletions descriptions/nodes/GH_RepoRunner.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Represents a self-hosted runner registered directly to a single GitHub repositor

The node captures runner metadata such as operating system, status, busy state, labels, and whether the runner is ephemeral when GitHub returns that property.

GH_RunsOn edges from GH_WorkflowJob nodes identify statically resolvable jobs in the containing repository that GitHub could schedule on this runner. These edges do not indicate that the job has actually executed on the runner.

## Properties

| Property | Type | Description |
Expand All @@ -31,13 +33,16 @@ The node captures runner metadata such as operating system, status, busy state,
| `environment_name` | `string` | The name of the environment (GitHub organization). |
| `query_group` | `string` | Query for group. |
| `query_repositories` | `string` | Query for repositories. |
| `query_jobs` | `string` | Query for workflow jobs that can be scheduled on the runner. |

## Diagram

```mermaid
graph LR
n0["GH_Repository"]
n1["GH_RepoRunner"]
n2["GH_WorkflowJob"]
n0 -.->|GH_CanUseRunner| n1
n0 -.->|GH_Contains| n1
n2 -.->|GH_RunsOn| n1
```
4 changes: 4 additions & 0 deletions descriptions/nodes/GH_Repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Represents a GitHub repository within the organization. Repository nodes capture metadata about the repo including visibility, Actions enablement status, and security configuration. Repository role nodes (GH_RepoRole) are created alongside each repository to represent the permission levels available.

For repositories with active workflows, the collector records the applicable default workflow permissions and whether workflows may approve pull request reviews. These properties preserve the repository-level policy input later used to derive effective GITHUB_TOKEN permissions for GH_WorkflowJob nodes.

## Properties

| Property | Type | Description |
Expand Down Expand Up @@ -40,6 +42,8 @@ Represents a GitHub repository within the organization. Repository nodes capture
| `secret_scanning` | `string` | Status of secret scanning (e.g., `enabled`, `disabled`). |
| `branch_ruleset_count` | `integer` | Number of branch-targeted rulesets that apply to this repository. |
| `has_branch_rulesets` | `boolean` | Whether at least one branch-targeted ruleset applies to this repository. |
| `default_workflow_permissions` | `string` | The repository's applicable default GITHUB_TOKEN workflow permissions. |
| `can_approve_pull_request_reviews` | `boolean` | Whether workflows may approve pull request reviews. |
| `query_branches` | `string` | Query for branches. |
| `query_protected_branches` | `string` | Query for protected branches. |
| `query_branch_protection_rules` | `string` | Query for branch protection rules. |
Expand Down
3 changes: 3 additions & 0 deletions descriptions/nodes/GH_Workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Represents a GitHub Actions workflow defined in a repository. Workflow nodes capture the workflow definition metadata including its file path, state, containing repository, and the full YAML contents of the workflow file. Only repositories with GitHub Actions enabled are queried for workflows.

When present, `workflow_permissions` captures the top-level `permissions` declaration from the workflow YAML.

## Properties

| Property | Type | Description |
Expand All @@ -25,6 +27,7 @@ Represents a GitHub Actions workflow defined in a repository. Workflow nodes cap
| `triggers` | `list[string]` | The triggers value. |
| `trigger_dispatch_inputs` | `list[string]` | The trigger dispatch inputs value. |
| `is_pwn_requestable` | `boolean` | The is pwn requestable value. |
| `workflow_permissions` | `list[string]` | Permissions declared at the workflow level. |
| `query_repository` | `string` | Query for repository. |
| `query_jobs` | `string` | Query for workflow jobs. |
| `query_execution` | `string` | Query for workflow executions. |
Expand Down
18 changes: 15 additions & 3 deletions descriptions/nodes/GH_WorkflowJob.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

Represents a single job within a GitHub Actions workflow. Jobs are the top-level execution units of a workflow — they run on a runner, hold a set of steps, and can declare permissions, environments, and dependencies on other jobs.

When the job has a statically resolvable self-hosted `runs-on` selector, GH_RunsOn edges identify each GH_Runner that currently satisfies the declared label and runner-group constraints under the repository's runner access policy. These edges represent schedulability, not historical execution.

When present, `job_permissions` captures the job-level `permissions` declaration from the workflow YAML. `effective_github_token_permissions` captures the calculated static `GITHUB_TOKEN` permissions after applying the repository default, workflow-level declaration, and job-level declaration.

## Properties

| Property | Type | Description |
Expand All @@ -15,10 +19,15 @@ Represents a single job within a GitHub Actions workflow. Jobs are the top-level
| `node_id` | `string` | The stable identifier used as the OpenGraph node ID; this is the native GitHub node ID where available. |
| `job_key` | `string` | The YAML key for the job. |
| `runs_on` | `list[string]` | The runner label expression for the job. |
| `runs_on_group` | `string` | The statically declared runner group, if any. |
| `runs_on_labels` | `list[string]` | The normalized runner labels from runs-on. |
| `runs_on_is_dynamic` | `boolean` | Whether runs-on contains a GitHub Actions expression. |
| `is_self_hosted` | `boolean` | Whether the job targets self-hosted runners. |
| `container` | `string` | The optional container configuration. |
| `environment` | `string` | The deployment environment name. |
| `permissions` | `list[string]` | Effective job permissions. |
| `permissions` | `list[string]` | Applicable declared workflow or job permissions after job-over-workflow precedence. |
| `job_permissions` | `list[string]` | Optional permissions declared at the job level; absent when the job has no declaration. |
| `effective_github_token_permissions` | `list[string]` | Calculated GITHUB_TOKEN permissions after repository defaults and declarations are applied. |
| `uses_reusable` | `string` | The reusable workflow reference used by this job. |
| `workflow_node_id` | `string` | The parent workflow node ID. |
| `repository_name` | `string` | The containing repository name. |
Expand All @@ -27,6 +36,7 @@ Represents a single job within a GitHub Actions workflow. Jobs are the top-level
| `query_repository` | `string` | Query for repository. |
| `query_steps` | `string` | Query for workflow steps. |
| `query_references` | `string` | Query for workflow references (secrets and variables). |
| `query_runners` | `string` | Query for eligible self-hosted runners. |

## Diagram

Expand All @@ -41,7 +51,8 @@ graph LR
n6["GH_OrgVariable"]
n7["GH_RepoSecret"]
n8["GH_RepoVariable"]
n9["GH_WorkflowStep"]
n9["GH_Runner"]
n10["GH_WorkflowStep"]
n0 -.->|GH_Contains| n1
n1 -.->|GH_DeploysTo| n2
n1 -.->|GH_UsesSecret| n3
Expand All @@ -50,7 +61,8 @@ graph LR
n1 -.->|GH_UsesVariable| n6
n1 -.->|GH_UsesSecret| n7
n1 -.->|GH_UsesVariable| n8
n1 -.->|GH_RunsOn| n9
n1 -.->|GH_CallsWorkflow| n0
n1 -.->|GH_DependsOn| n1
n1 -.->|GH_Contains| n9
n1 -.->|GH_Contains| n10
```
30 changes: 17 additions & 13 deletions extension/saved_searches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,25 @@ Pre-built Cypher queries for identifying security-relevant configurations across
| 39 | `dangerous-branch-perms.json` | Dangerous Branch Permissions | Identifies users with dangerous branch permissions in a GitHub organization, including bypass allowances on protection rules. |
| 40 | `org-roles-bypass-security-scanning.json` | Org Roles That Can Bypass Security Scanning | Finds organization roles with permissions to bypass or manage security scanning dismissals. These roles can suppress secret scanning and code scanning findings. |
| 41 | `github-to-azure-identity.json` | GitHub-to-Azure Identity Assumptions | Finds GitHub entities (repositories, branches, environments) that can assume Azure identities via OIDC federation. Verify that each trust relationship is intentional and scoped appropriately. |
| 42 | `workflow-jobs-with-id-token-write.json` | Workflow Jobs with OIDC Token Permission | Returns workflow jobs whose effective GITHUB_TOKEN permissions include `id-token:write`. |
| 43 | `workflow-jobs-with-id-token-write-on-self-hosted-runners.json` | OIDC-Capable Workflow Jobs on Self-Hosted Runners | Returns OIDC-capable jobs that can be scheduled on collected self-hosted runners. |
| 44 | `workflow-jobs-with-broad-token-write-permissions.json` | Workflow Jobs with Broad GITHUB_TOKEN Write Permissions | Returns jobs with effective write access to repository contents, Actions, or pull requests. |
| 45 | `workflow-jobs-with-observed-oidc-auth-steps.json` | Workflow Jobs with Observed OIDC Authentication Steps | Returns OIDC-capable jobs with descendant steps that show likely token consumption. |

### :white_circle: Low — Hygiene & Governance

| # | File | Name | Description |
|---|------|------|-------------|
| 42 | `environments-admin-bypass.json` | Environments Where Admins Can Bypass Protections | Finds deployment environments where administrators can bypass protection rules such as required reviewers and wait timers. Admins can deploy to these environments without any approval. |
| 43 | `app-installations-all-repos.json` | App Installations with Access to All Repositories | Finds GitHub App installations that have access to every repository in the organization. A compromised app credential would affect all repositories. |
| 44 | `users-without-external-identity.json` | GitHub Users Without External Identity Mapping | Finds GitHub users that are not linked to any external identity via SAML or SCIM. These users cannot be centrally offboarded through the identity provider and may retain access after employment ends. |
| 45 | `external-identities-without-scim.json` | External Identities Without SCIM Provisioning | Finds external identities that lack SCIM synchronization. Without SCIM, user deprovisioning in the identity provider will not automatically revoke GitHub access. |
| 46 | `org-owners.json` | Organization Owners | Returns all users who hold the organization owners role. |
| 47 | `privileged-custom-org-roles.json` | Privileged Custom Org Roles | Returns all custom organization roles that are privileged (i.e., have permissions that are not default). |
| 48 | `global-repo-perms.json` | Global Repo Permissions | Returns all users who hold a global repository permission role (i.e., roles that are not default). |
| 49 | `hybrid-identities.json` | External Identities | Returns all external identities (e.g., Azure or Okta users) that are associated with GitHub users. |
| 50 | `privileged-hybrid-identities.json` | Privileged Hybrid Identities | Returns all hybrid identities (e.g., Azure or Okta users) that are associated with GitHub users who hold the organization owners role. |
| 51 | `saml-configuration.json` | SAML Configuration Mapping | Finds SAML Identity Providers, their external identities, and mapped users. |
| 52 | `team-membership-admin.json` | Team Membership Admins | Returns all users who hold the maintainer role over a team, including team nesting. |
| 53 | `team-structure.json` | Team Structure | Returns the structure of teams within organizations, including team roles and their members. |
| 54 | `repository-workflows.json` | Repository Workflows | Returns all repository workflows. |
| 46 | `environments-admin-bypass.json` | Environments Where Admins Can Bypass Protections | Finds deployment environments where administrators can bypass protection rules such as required reviewers and wait timers. Admins can deploy to these environments without any approval. |
| 47 | `app-installations-all-repos.json` | App Installations with Access to All Repositories | Finds GitHub App installations that have access to every repository in the organization. A compromised app credential would affect all repositories. |
| 48 | `users-without-external-identity.json` | GitHub Users Without External Identity Mapping | Finds GitHub users that are not linked to any external identity via SAML or SCIM. These users cannot be centrally offboarded through the identity provider and may retain access after employment ends. |
| 49 | `external-identities-without-scim.json` | External Identities Without SCIM Provisioning | Finds external identities that lack SCIM synchronization. Without SCIM, user deprovisioning in the identity provider will not automatically revoke GitHub access. |
| 50 | `org-owners.json` | Organization Owners | Returns all users who hold the organization owners role. |
| 51 | `privileged-custom-org-roles.json` | Privileged Custom Org Roles | Returns all custom organization roles that are privileged (i.e., have permissions that are not default). |
| 52 | `global-repo-perms.json` | Global Repo Permissions | Returns all users who hold a global repository permission role (i.e., roles that are not default). |
| 53 | `hybrid-identities.json` | External Identities | Returns all external identities (e.g., Azure or Okta users) that are associated with GitHub users. |
| 54 | `privileged-hybrid-identities.json` | Privileged Hybrid Identities | Returns all hybrid identities (e.g., Azure or Okta users) that are associated with GitHub users who hold the organization owners role. |
| 55 | `saml-configuration.json` | SAML Configuration Mapping | Finds SAML Identity Providers, their external identities, and mapped users. |
| 56 | `team-membership-admin.json` | Team Membership Admins | Returns all users who hold the maintainer role over a team, including team nesting. |
| 57 | `team-structure.json` | Team Structure | Returns the structure of teams within organizations, including team roles and their members. |
| 58 | `repository-workflows.json` | Repository Workflows | Returns all repository workflows. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "GitHub: Workflow Jobs with Broad GITHUB_TOKEN Write Permissions",
"query": "MATCH p=(repo:GH_Repository)-[:GH_Contains]->(:GH_Workflow)-[:GH_Contains]->(job:GH_WorkflowJob)\nWHERE 'contents:write' IN job.effective_github_token_permissions\nOR 'actions:write' IN job.effective_github_token_permissions\nOR 'pull-requests:write' IN job.effective_github_token_permissions\nRETURN p\nLIMIT 1000",
"description": "Returns workflow jobs whose calculated effective GITHUB_TOKEN permissions include write access to repository contents, Actions, or pull requests. These permissions can materially increase the impact of workflow compromise."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "GitHub: OIDC-Capable Workflow Jobs on Self-Hosted Runners",
"query": "MATCH p=(repo:GH_Repository)-[:GH_Contains]->(:GH_Workflow)-[:GH_Contains]->(job:GH_WorkflowJob)-[:GH_RunsOn]->(:GH_Runner)\nWHERE 'id-token:write' IN job.effective_github_token_permissions\nRETURN p\nLIMIT 1000",
"description": "Returns OIDC-capable workflow jobs that can be scheduled on collected self-hosted runners. These jobs are especially important to review because runner compromise could expose short-lived cloud federation tokens."
}
Loading
Loading