Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5938e29
BED-9674: link workflow jobs to eligible runners
jaredcatkinson Sep 6, 2026
7d89b3c
Merge pull request #58 from jaredcatkinson/feature/BED-9674-link-work…
jaredcatkinson Sep 6, 2026
11e7731
BED-9674: suppress runner matches when actions disabled
jaredcatkinson Sep 6, 2026
2b939b7
Merge pull request #60 from jaredcatkinson/feature/BED-9674-link-work…
jaredcatkinson Sep 6, 2026
ddc6f46
BED-9674: normalize self-hosted selector casing
jaredcatkinson Sep 6, 2026
914b0a3
Merge remote-tracking branch 'upstream/feature/BED-9674-link-workflow…
jaredcatkinson Sep 6, 2026
682c93a
BED-9675: model effective GitHub token permissions
jaredcatkinson Sep 6, 2026
d0a366c
Merge pull request #61 from jaredcatkinson/feature/BED-9675-effective…
jaredcatkinson Sep 6, 2026
d974094
BED-9675: address workflow permission review feedback
jaredcatkinson Sep 7, 2026
9cbdf2b
Merge pull request #63 from jaredcatkinson/feature/BED-9675-effective…
jaredcatkinson Sep 7, 2026
0b37c09
BED-9677: model workflow job interception by runners
jaredcatkinson Sep 7, 2026
80dc31e
BED-9677: preserve inherited runner group restriction lookup
jaredcatkinson Sep 7, 2026
ecdfc55
BED-9678: model workflow job environment OIDC capability
jaredcatkinson Sep 7, 2026
d995ad7
BED-9678: address workflow OIDC review feedback
jaredcatkinson Sep 7, 2026
88725c7
Merge upstream/main into feature/BED-9674-link-workflow-jobs-runners
jaredcatkinson Sep 8, 2026
42a934c
Merge upstream/main into feature/BED-9675-effective-github-token-perm…
jaredcatkinson Sep 8, 2026
71ce547
Merge feature/BED-9674-link-workflow-jobs-runners into feature/BED-96…
jaredcatkinson Sep 8, 2026
13b104e
BED-9675: clarify workflow job permission docs
jaredcatkinson Sep 8, 2026
99b40c0
Merge feature/BED-9675-effective-github-token-permissions into featur…
jaredcatkinson Sep 8, 2026
dfb2685
BED-9677: clarify dynamic runner interception status
jaredcatkinson Sep 8, 2026
25c105d
Merge feature/BED-9677-runner-job-interception into fix/BED-9678-job-…
jaredcatkinson Sep 8, 2026
8ae249a
BED-9678: cover workflow permission cache isolation
jaredcatkinson Sep 8, 2026
41ee6c6
BED-9678: cover repository-specific workflow cache keys
jaredcatkinson Sep 8, 2026
3da964d
BED-9678: clarify static OIDC capability semantics
jaredcatkinson Sep 8, 2026
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
30 changes: 30 additions & 0 deletions descriptions/edges/GH_CanAccessSecret.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# GH_CanAccessSecret

## General Information

The traversable GH_CanAccessSecret edge represents that a GitHub Actions workflow job execution context can access a statically referenced secret.

This edge is derived from the existing non-traversable GH_UsesSecret relationships on the job's contained steps and from job-level `env` declarations. It is intended for attack-path analysis from a compromised job execution context to the secrets that context can read.

The collector only emits this edge when one of the workflow job's modeled steps or the job's `env` block statically references the secret. The existence of a secret in the repository, organization, or environment scope alone is not enough. For this initial implementation, secrets passed through `jobs.<job_id>.secrets` to reusable workflows are retained as structural references but are not projected as runtime access for the caller job.

## Edge Schema

| Source | Destination | Traversable |
| --- | --- | --- |
| `GH_WorkflowJob` | `GH_EnvironmentSecret` | `true` |
| `GH_WorkflowJob` | `GH_OrgSecret` | `true` |
| `GH_WorkflowJob` | `GH_RepoSecret` | `true` |

## Diagram

```mermaid
graph LR
n0["GH_WorkflowJob"]
n1["GH_EnvironmentSecret"]
n2["GH_OrgSecret"]
n3["GH_RepoSecret"]
n0 -->|GH_CanAccessSecret| n1
n0 -->|GH_CanAccessSecret| n2
n0 -->|GH_CanAccessSecret| n3
```
24 changes: 24 additions & 0 deletions descriptions/edges/GH_CanInterceptJob.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# GH_CanInterceptJob

## General Information

The traversable GH_CanInterceptJob edge represents that a self-hosted runner not explicitly marked ephemeral can intercept a GitHub Actions workflow job that GitHub could schedule on it.

This edge is derived from GH_RunsOn and is intended for attack-path analysis. It does not mean that the job has historically executed on the runner. It means that control of the runner may expose the future execution context of the job when the runner is not ephemeral.

The collector does not emit this edge for runners GitHub explicitly marks as ephemeral.

## Edge Schema

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

## Diagram

```mermaid
graph LR
n0["GH_Runner"]
n1["GH_WorkflowJob"]
n0 -->|GH_CanInterceptJob| n1
```
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
26 changes: 26 additions & 0 deletions descriptions/edges/GH_CanRequestOIDCTokenFor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# GH_CanRequestOIDCTokenFor

## General Information

The traversable GH_CanRequestOIDCTokenFor edge represents a static upper-bound capability: based on the collected workflow configuration, a GitHub Actions workflow job execution context may be able to request a GitHub-signed OIDC token containing claims for its associated GitHub Environment.

This edge is derived from the existing GH_DeploysTo relationship and the job's calculated `effective_github_token_permissions`. The collector emits it only when the job targets a statically resolved environment and its effective permissions include `id-token:write`.

This is a capability edge, not evidence that the workflow has historically requested a token or contains an explicit OIDC-related step. Code executing in a job with `id-token:write` can request the token directly.

The edge does not model run-specific permission recalculation. In particular, a `pull_request` run originating from a fork may receive downgraded `GITHUB_TOKEN` permissions at runtime and therefore may not be able to request an OIDC token even when this edge exists. The edge should not be interpreted as proof that every execution of the job can request OIDC.

## Edge Schema

| Source | Destination | Traversable |
| --- | --- | --- |
| `GH_WorkflowJob` | `GH_Environment` | `true` |

## Diagram

```mermaid
graph LR
n0["GH_WorkflowJob"]
n1["GH_Environment"]
n0 -->|GH_CanRequestOIDCTokenFor| n1
```
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
```
9 changes: 9 additions & 0 deletions descriptions/nodes/GH_EnterpriseRunner.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ 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.

When the runner is not explicitly marked ephemeral, GH_CanInterceptJob edges identify workflow jobs whose future execution context may be exposed to an actor controlling the runner.

## Properties

| Property | Type | Description |
Expand All @@ -31,13 +35,18 @@ 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. |
| `query_interceptable_jobs` | `string` | Query for workflow jobs the runner can intercept. |

## 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
n1 -->|GH_CanInterceptJob| n2
```
3 changes: 3 additions & 0 deletions descriptions/nodes/GH_Environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Represents a GitHub Actions deployment environment configured on a repository. E

Repositories always contain their environments. When custom branch policies are configured, the environment also contains one or more GH_EnvironmentBranchPolicy nodes that describe which branches are allowed to deploy. Environment-scoped secrets and variables are modeled as child nodes of the environment and become available to workflow jobs that reference it.

GH_CanRequestOIDCTokenFor edges from GH_WorkflowJob nodes identify jobs with a static upper-bound capability to request a GitHub-signed OIDC token for this environment because their effective `GITHUB_TOKEN` permissions include `id-token:write`. Runtime permission recalculation, such as forked `pull_request` permission downgrades, may prevent a specific execution from requesting OIDC even when this edge exists.

## Properties

| Property | Type | Description |
Expand Down Expand Up @@ -57,4 +59,5 @@ graph LR
n8 -.->|GH_ApprovesDeploymentTo| n1
n8 -->|GH_CanDeployToEnvironment| n1
n9 -.->|GH_DeploysTo| n1
n9 -->|GH_CanRequestOIDCTokenFor| n1
```
9 changes: 9 additions & 0 deletions descriptions/nodes/GH_OrgRunner.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ 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.

When the runner is not explicitly marked ephemeral, GH_CanInterceptJob edges identify workflow jobs whose future execution context may be exposed to an actor controlling the runner.

## Properties

| Property | Type | Description |
Expand All @@ -31,13 +35,18 @@ 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. |
| `query_interceptable_jobs` | `string` | Query for workflow jobs the runner can intercept. |

## 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
n1 -->|GH_CanInterceptJob| n2
```
9 changes: 9 additions & 0 deletions descriptions/nodes/GH_RepoRunner.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ 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.

When the runner is not explicitly marked ephemeral, GH_CanInterceptJob edges identify workflow jobs whose future execution context may be exposed to an actor controlling the runner.

## Properties

| Property | Type | Description |
Expand All @@ -31,13 +35,18 @@ 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. |
| `query_interceptable_jobs` | `string` | Query for workflow jobs the runner can intercept. |

## 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
n1 -->|GH_CanInterceptJob| n2
```
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
28 changes: 25 additions & 3 deletions descriptions/nodes/GH_WorkflowJob.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

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.

GH_CanAccessSecret edges identify secrets statically referenced by the job's modeled steps or job-level `env` block that the job execution context can access. GH_CanInterceptJob edges from GH_Runner nodes not explicitly marked ephemeral identify jobs whose future execution context may be exposed if that runner is controlled. When a job targets an environment and its effective permissions include `id-token:write`, GH_CanRequestOIDCTokenFor identifies a static upper-bound OIDC capability for that environment. Runtime permission recalculation, such as forked `pull_request` permission downgrades, may prevent a specific execution from requesting OIDC even when this edge exists.

When `runs_on_is_dynamic` is true, runner matching and interception status remain unresolved: the collector does not emit GH_CanInterceptJob edges for the job, so `query_interceptable_jobs` cannot match it and the absence of an edge must not be treated as evidence that the job is definitively non-interceptable.

## Properties

| Property | Type | Description |
Expand All @@ -15,10 +23,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 +40,8 @@ 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. |
| `query_accessible_secrets` | `string` | Query for secrets accessible to the job execution context. |

## Diagram

Expand All @@ -41,7 +56,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 +66,13 @@ graph LR
n1 -.->|GH_UsesVariable| n6
n1 -.->|GH_UsesSecret| n7
n1 -.->|GH_UsesVariable| n8
n1 -.->|GH_RunsOn| n9
n1 -->|GH_CanAccessSecret| n3
n1 -->|GH_CanAccessSecret| n5
n1 -->|GH_CanAccessSecret| n7
n1 -->|GH_CanRequestOIDCTokenFor| n2
n9 -->|GH_CanInterceptJob| n1
n1 -.->|GH_CallsWorkflow| n0
n1 -.->|GH_DependsOn| n1
n1 -.->|GH_Contains| n9
n1 -.->|GH_Contains| n10
```
Loading
Loading