Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
7 changes: 7 additions & 0 deletions descriptions/edges/GH_ApprovesDeploymentTo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## General Information

The non-traversable GH_ApprovesDeploymentTo edge represents that a user or team is configured as a required reviewer for a GitHub Environment.

This edge is emitted from GH_User or GH_Team nodes to GH_Environment nodes when the environment includes a required reviewer protection rule. Required reviewers act as an approval gate before jobs referencing the environment can continue.

The edge is non-traversable because being listed as a reviewer does not by itself grant deployment access, but it is important context for understanding how deployments are approved and which identities participate in environment protection workflows.
5 changes: 5 additions & 0 deletions descriptions/edges/GH_CanCreateEnvironment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## General Information

The traversable GH_CanCreateEnvironment edge is a computed edge indicating that a repository role can cause a new GitHub environment to be created in the repository. This is derived from the ability to create and modify runnable branches/workflows: if a workflow references an environment name that does not already exist, GitHub will create that environment automatically.

This edge is useful for modeling OIDC and deployment scenarios where trust is tied to an environment name. An attacker who can create a new environment through workflow changes may be able to instantiate a trusted environment on demand, even if it was not previously configured.
7 changes: 7 additions & 0 deletions descriptions/edges/GH_CanDeployToEnvironment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## General Information

The traversable GH_CanDeployToEnvironment edge represents the ability for a repository, branch, or repository role to satisfy the modeled deployment branch policy or administrator bypass condition for a GitHub Environment.

This edge is computed from environment deployment branch policy, branch protection state, and administrator bypass behavior. For unrestricted environments, the repository and all contained branches may receive this edge. For protected-branch-only environments, only protected branches receive it unless the repository has no branch protection rules at all, in which case all branches can deploy. For custom branch policies, only branches matching a GH_EnvironmentBranchPolicy receive the edge.

Required reviewers and wait timers are represented as environment metadata and GH_ApprovesDeploymentTo context rather than as additional GH_CanDeployToEnvironment edges. A branch edge therefore means the branch is eligible to target the environment, not that every runtime approval or timing gate has already been satisfied.
15 changes: 15 additions & 0 deletions descriptions/edges/GH_CanEditEnvironment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## General Information

The traversable GH_CanEditEnvironment edge indicates that a repository role can modify the configuration of a GitHub environment. In the current model, this edge is emitted for the repository's built-in `admin` role to every environment contained in that repository.

Editing an environment is security-relevant because environment configuration controls deployment protections such as required reviewers, self-review restrictions, wait timers, deployment branch policies, and the "allow administrators to bypass configured protection rules" setting. An attacker who can edit an environment may be able to weaken or remove those controls, making later deployment and secret access paths possible.

This edge is distinct from GH_CanDeployToEnvironment:

- **GH_CanEditEnvironment** means the role can manage the environment's settings.
- **GH_CanDeployToEnvironment** means the source satisfies the modeled deployment branch policy or administrator bypass condition for the environment.

```mermaid
graph LR
role["GH_RepoRole admin"] ==>|GH_CanEditEnvironment| env["GH_Environment production"]
```
2 changes: 1 addition & 1 deletion descriptions/edges/GH_Contains.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## General Information

The non-traversable GH_Contains edge represents structural containment within the GitHub resource hierarchy. The organization serves as the top-level container for users, teams, repositories, roles, secrets, app installations, and personal access tokens. Repositories contain their own repo-level secrets, and environments contain environment-scoped secrets. This edge is created by the collector to establish the organizational hierarchy of GitHub resources 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 organization serves as the top-level container for users, teams, repositories, roles, secrets, app installations, and personal access tokens. Repositories contain branches, workflows, branch protection rules, environments, and repo-level secrets and variables. 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.
2 changes: 1 addition & 1 deletion descriptions/edges/GH_DeploysTo.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## General Information

The non-traversable GH_DeploysTo edge links a workflow job to the GitHub Environment it targets via the `environment:` key. This edge records which jobs deploy to which environments. Environments can gate deployments with protection rules (required reviewers, wait timers, deployment branch policies) and can expose environment-scoped secrets.
The non-traversable GH_DeploysTo edge links a workflow job to the GitHub Environment it targets via the `environment:` key. This edge records which jobs deploy to which environments. Environments can gate deployments with protection rules (required reviewers, wait timers, deployment branch policies) and can expose environment-scoped secrets and variables.
2 changes: 1 addition & 1 deletion descriptions/edges/GH_HasBranch.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## General Information

The non-traversable GH_HasBranch edge represents the relationship between a repository and its branches. This edge links each collected branch to its parent repository. It is a structural edge that provides the foundation for understanding branch-level protections and access controls. While not traversable itself, it connects repositories to branches where traversable edges like GH_CanWriteBranch and GH_CanEditProtection model the effective access.
The non-traversable GH_Contains edge represents the relationship between a repository and its branches. This edge links each collected branch to its parent repository. It is a structural edge that provides the foundation for understanding branch-level protections and access controls. While not traversable itself, it connects repositories to branches where traversable edges like GH_CanWriteBranch and GH_CanEditProtection model the effective access.
2 changes: 1 addition & 1 deletion descriptions/edges/GH_HasEnvironment.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## General Information

The non-traversable GH_HasEnvironment edge represents the relationship between a repository or branch and its deployment environments. This edge links environments to the repositories that define them and to the branches that are allowed to deploy to them (via deployment branch policies). Environments are security-relevant because they can gate access to secrets and cloud credentials, and their deployment branch policies control which branches can trigger deployments.
Environment containment is modeled with the non-traversable GH_Contains edge from a repository to each deployment environment it defines. Branch deployment eligibility is modeled separately with GH_CanDeployToEnvironment and GH_MatchesEnvironmentPolicy rather than a containment edge. Environments are security-relevant because they can gate access to secrets and cloud credentials, and their deployment branch policies control which branches can trigger deployments.
2 changes: 1 addition & 1 deletion descriptions/edges/GH_HasSecret.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## General Information

The traversable GH_HasSecret edge represents the relationship between a repository or environment and the secrets accessible within that context. This edge shows which secrets are available in which scopes. Repositories can have access to both organization-level secrets (scoped to selected repositories) and repository-level secrets, while environments contain their own environment-scoped secrets. This edge is traversable because any principal that can push code to a repository (via GH_CanWriteBranch or GH_CanCreateBranch) can write a workflow that exfiltrates the secret values at runtime, making this a meaningful link in attack path analysis.
The traversable GH_HasSecret edge represents the relationship between a repository or environment and the secrets accessible within that context. This edge shows which secrets are available in which scopes. Repositories can have access to both organization-level secrets (scoped to selected repositories) and repository-level secrets, while environments expose their own environment-scoped secrets to jobs that target them. This edge is traversable because any principal that can execute a workflow in the relevant context may be able to exfiltrate secret values at runtime, making this a meaningful link in attack path analysis.
2 changes: 1 addition & 1 deletion descriptions/edges/GH_HasVariable.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## General Information

The traversable GH_HasVariable edge represents the relationship between a repository and the variables accessible within that context. This edge shows which variables are available in which scopes. Repositories can have access to both organization-level variables (scoped by visibility to all, private, or selected repositories) and repository-level variables defined directly on the repo. This edge is traversable because any principal that can push code to a repository (via GH_CanWriteBranch or GH_CanCreateBranch) can write a workflow that reads variable values at runtime, and variables may contain configuration data useful for lateral movement such as deployment URLs, service names, or environment identifiers.
The traversable GH_HasVariable edge represents the relationship between a repository or environment and the variables accessible within that context. This edge shows which variables are available in which scopes. Repositories can have access to both organization-level variables (scoped by visibility to all, private, or selected repositories) and repository-level variables defined directly on the repo, while environments expose their own environment-scoped variables to jobs that target them. This edge is traversable because any principal that can execute a workflow in the relevant context may be able to read variable values at runtime, and variables may contain configuration data useful for lateral movement such as deployment URLs, service names, or environment identifiers.
7 changes: 7 additions & 0 deletions descriptions/edges/GH_MatchesEnvironmentPolicy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## General Information

The non-traversable GH_MatchesEnvironmentPolicy edge links a branch to a GitHub Environment deployment branch policy that it satisfies.

This edge is emitted when a branch name matches the pattern defined by a GH_EnvironmentBranchPolicy node, such as `main`, `release/*`, or `release/**/*`. The edge is structural rather than directly traversable because matching a policy alone does not guarantee deployment access; the environment may still require protected branches, reviewers, wait timers, or other controls.

GH_MatchesEnvironmentPolicy is primarily used as supporting evidence for computed GH_CanDeployToEnvironment edges.
7 changes: 4 additions & 3 deletions descriptions/edges/GH_UsesSecret.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
## General Information

The traversable GH_UsesSecret edge links a workflow step to the secret it references via a `${{ secrets.NAME }}` expression. This edge reveals which secrets a step can access at runtime, enabling analysts to trace the blast radius of a compromised workflow.
The non-traversable GH_UsesSecret edge links a workflow job or step to the secret it references via a `${{ secrets.NAME }}` expression. This edge reveals which secrets a workflow component can access at runtime, enabling analysts to trace the blast radius of a compromised workflow.

### Matching strategy

Edges use `match_by: property` with two matchers to disambiguate between secrets with the same name across repositories:
Edges use `match_by: property` with scope-specific matchers to disambiguate between secrets with the same name across repositories and environments:

- **GH_RepoSecret** is matched by `name` + `repository_id` (the GitHub node_id of the repository).
- **GH_OrgSecret** is matched by `name` + `environmentid` (the node_id of the organization, which acts as the org-level secret scope).
- **GH_EnvironmentSecret** is matched by `name` + `deployment_environment_name` + `repository_id` when the parent job targets a concrete environment name.

This means one `${{ secrets.MY_SECRET }}` expression in a workflow can produce up to two GH_UsesSecret edges — one to the repo-level secret and one to the org-level secret — reflecting that either could supply the value at runtime depending on scope precedence.
This means one `${{ secrets.MY_SECRET }}` expression in a workflow can produce edges to repo-level, org-level, and environment-level secrets that share the same name in the applicable scopes. The environment-level edge is only emitted when the workflow job references a literal environment name rather than a dynamic expression.

### Context property

Expand Down
7 changes: 4 additions & 3 deletions descriptions/edges/GH_UsesVariable.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
## General Information

The non-traversable GH_UsesVariable edge links a workflow step to the variable it references via a `${{ vars.NAME }}` expression. This edge maps variable consumption within workflows. Unlike secrets, variable values are readable via the API, making them lower sensitivity — but they can still influence workflow behavior (e.g., controlling target environments or feature flags).
The non-traversable GH_UsesVariable edge links a workflow job or step to the variable it references via a `${{ vars.NAME }}` expression. This edge maps variable consumption within workflows. Unlike secrets, variable values are readable via the API, making them lower sensitivity — but they can still influence workflow behavior (e.g., controlling target environments or feature flags).

### Matching strategy

Edges use `match_by: property` with two matchers to disambiguate between variables with the same name across repositories:
Edges use `match_by: property` with scope-specific matchers to disambiguate between variables with the same name across repositories and environments:

- **GH_RepoVariable** is matched by `name` + `repository_id` (the GitHub node_id of the repository).
- **GH_OrgVariable** is matched by `name` + `environmentid` (the node_id of the organization, which acts as the org-level variable scope).
- **GH_EnvironmentVariable** is matched by `name` + `deployment_environment_name` + `repository_id` when the parent job targets a concrete environment name.

This means one `${{ vars.MY_VAR }}` expression can produce up to two GH_UsesVariable edges — one to the repo-level variable and one to the org-level variable.
This means one `${{ vars.MY_VAR }}` expression can produce edges to repo-level, org-level, and environment-level variables that share the same name in the applicable scopes. The environment-level edge is only emitted when the workflow job references a literal environment name rather than a dynamic expression.

### Context property

Expand Down
4 changes: 3 additions & 1 deletion descriptions/nodes/GH_Environment.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## Description

Represents a GitHub Actions deployment environment configured on a repository. Environments can have protection rules including required reviewers, wait timers, and deployment branch policies. When custom branch policies are configured, the environment is connected to specific branches; otherwise, it is connected directly to the repository.
Represents a GitHub Actions deployment environment configured on a repository. Environments can have protection rules including required reviewers, wait timers, administrator bypass behavior, and deployment branch policies.

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.
5 changes: 5 additions & 0 deletions descriptions/nodes/GH_EnvironmentBranchPolicy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Description

Represents a deployment branch policy attached to a GitHub Environment. These policies define which branches or branch patterns are allowed to deploy to the environment, such as `main`, `release/*`, or `release/**/*`.

Environment branch policies are modeled as their own nodes so analysts can distinguish between the environment itself and the matching rules that govern deployment eligibility.
2 changes: 2 additions & 0 deletions descriptions/nodes/GH_EnvironmentSecret.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## Description

Represents an environment-level GitHub Actions secret. These secrets are scoped to a specific deployment environment and are only available to workflow jobs that reference that environment.

The containing environment is linked to the secret with GH_Contains and GH_HasSecret edges. Workflow steps that reference the secret by name receive GH_UsesSecret edges when their job targets the same environment.
2 changes: 2 additions & 0 deletions descriptions/nodes/GH_EnvironmentVariable.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## Description

Represents an environment-level GitHub Actions variable. These variables are scoped to a specific deployment environment and are only available to workflow jobs that reference that environment. Unlike secrets, variable values are readable via the API.

The containing environment is linked to the variable with GH_Contains and GH_HasVariable edges. Workflow steps that reference the variable by name receive GH_UsesVariable edges when their job targets the same environment.
2 changes: 1 addition & 1 deletion extension/saved_searches/environments-admin-bypass.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "GitHub: Environments Where Admins Can Bypass Protections",
"query": "MATCH p=(:GH_Repository)-[:GH_HasEnvironment]->(env:GH_Environment {can_admins_bypass: true})\nRETURN p\nLIMIT 1000",
"query": "MATCH p=(:GH_Repository)-[:GH_Contains]->(env:GH_Environment {can_admins_bypass: true})\nRETURN p\nLIMIT 1000",
"description": "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."
}
2 changes: 1 addition & 1 deletion extension/saved_searches/repository-workflows.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "GitHub: Repository Workflows",
"query": "MATCH p=(:GH_Repository)-[:GH_HasWorkflow]->(:GH_Workflow)\nRETURN p\nLIMIT 1000",
"query": "MATCH p=(:GH_Repository)-[:GH_Contains]->(:GH_Workflow)\nRETURN p\nLIMIT 1000",
"description": "Returns all repository workflows"
}
2 changes: 1 addition & 1 deletion extension/saved_searches/unprotected-branches.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "GitHub: Unprotected Branches",
"query": "MATCH p=(repo:GH_Repository)-[:GH_HasBranch]-(:GH_Branch {protected: false})\nRETURN p\nLIMIT 1000",
"query": "MATCH p=(repo:GH_Repository)-[:GH_Contains]-(:GH_Branch {protected: false})\nRETURN p\nLIMIT 1000",
"description": "Returns all unprotected branches in repositories."
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "GitHub: Repositories with Workflows and Unprotected Default Branch",
"query": "MATCH p=(repo:GH_Repository)-[:GH_HasWorkflow]->(:GH_Workflow)\nMATCH p1=(repo)-[:GH_HasBranch]->(branch:GH_Branch)\nWHERE repo.default_branch = branch.short_name\nAND branch.protected = false\nRETURN p1\nLIMIT 1000",
"query": "MATCH p=(repo:GH_Repository)-[:GH_Contains]->(:GH_Workflow)\nMATCH p1=(repo)-[:GH_Contains]->(branch:GH_Branch)\nWHERE repo.default_branch = branch.short_name\nAND branch.protected = false\nRETURN p1\nLIMIT 1000",
"description": "Returns all repositories that have GitHub Actions workflows and an unprotected default branch. This means that users with GH_WriteRepoContents to the Repository can overwrite or change the workflow."
}
2 changes: 1 addition & 1 deletion extension/saved_searches/unprotected-default-branches.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "GitHub: Unprotected Default Branches",
"query": "MATCH p=(repo:GH_Repository)-[:GH_HasBranch]-(branch:GH_Branch {protected: false})\nWHERE repo.default_branch = branch.short_name\nRETURN p\nLIMIT 1000",
"query": "MATCH p=(repo:GH_Repository)-[:GH_Contains]-(branch:GH_Branch {protected: false})\nWHERE repo.default_branch = branch.short_name\nRETURN p\nLIMIT 1000",
"description": "Returns all default branches in repositories that are not protected."
}
Loading