-
Notifications
You must be signed in to change notification settings - Fork 28
OpenGraph Node Edge Tables
Table reference for the node and edge types emitted by GCPwn OpenGraph.
Coverage note:
- Some edge kinds are rule-driven from
og_privilege_escalation_paths.json. - Some node types are generated dynamically from resource types.
- This page documents the families emitted by the pipeline stages (
stage_1_principals->stage_5_allowlist_trim) plus the built-in rule kinds. Theprincipal_type()/gcp_resource_node_type()helpers incore_helpers.pydecide identity/resource node kinds; the dangerous-rule edge kinds come fromog_privilege_escalation_paths.jsonviaiam_bindings_shared_helpers.py.
principal_type() (core_helpers.py) maps a canonical IAM member token to its primary BloodHound kind. A principal first seen as the generic GCPPrincipal is upgraded in place when its prefix becomes known.
| Node Type | Triggered by (principal token) | Emitted By |
|---|---|---|
GoogleUser |
user:<email> |
Workspace users stage + IAM binding stages |
GoogleGroup |
group:<email> |
Workspace groups stage + IAM binding stages |
GoogleWorkspaceTenant |
tenant hub node workspace_tenant:<customer>
|
domain-wide-delegation stage (SA→tenant, users→tenant) |
PrincipalsInOrg |
principals_in_org:<customer> (any authenticated user in the tenant) |
group-settings stage (all-in-domain-can-join origin) |
GCPServiceAccount |
serviceAccount:<email> |
IAM binding + resource expansion + principal-set stages |
GCPDomainPrincipal |
domain:<domain> |
principal stage (only meaningful once a domain: node exists) |
GCPAllUsers |
the literal allUsers member |
IAM binding stage |
GCPAllAuthenticatedUsers |
the literal allAuthenticatedUsers member |
IAM binding stage |
GCPPrincipalSet |
principalSet://... (CRM SA sets, WIF group/attribute/* sets) |
IAM binding + resource expansion stages |
GCPConvenienceMember |
projectOwner: / projectEditor: / projectViewer:
|
fallback type only; these are expanded to concrete principals and not materialized as standalone identity nodes |
GCPPrincipal |
generic fallback, including principal://... selector members not matched above |
IAM binding stage; upgraded to a specific kind when the prefix is learned |
| Node Type | Meaning | Emitted By |
|---|---|---|
GCPProject |
Project resource node | IAM binding + resource expansion stages |
GCPFolder |
Folder resource node | IAM binding stage |
GCPOrganization |
Organization resource node | IAM binding stage |
GCPIamSimpleBinding |
Binding node for the principal -> binding -> resource model (one grant satisfies a path) |
IAM binding stage (single-permission); also the implied-grant node in the inferred stage |
GCPIamMultiBinding |
Combo binding node when several distinct-role bindings together satisfy a rule | IAM binding multi-permission (combo) stage |
GCPIamCapability |
Intermediate capability node for combo-hop rules (node_mode: capability) |
IAM binding multi-permission stage |
GCPServiceAccountKey |
Service account key node | resource expansion stage |
GCPExternalIdentitySource |
Synthetic external-identity source highlighting broad/no-condition federation trust (e.g. GitHubNoCondition) |
resource expansion stage |
GCPResource |
Generic resource fallback when the resource type is known-but-unmapped | resource/edge target helpers |
GCPUnknown |
Last-resort node kind when no type can be derived | node-kind helper |
GCPIamBinding / GCPIamGrant
|
Compatibility/internal IAM node aliases used by styling and export logic | internal compatibility layer |
Resource node kinds come from gcp_resource_node_type() (RESOURCE_TOKEN_TO_NODE_TYPE plus a GCP<Token> fallback). Major families:
| Node Type | Example resource family |
|---|---|
GCPBucket |
Cloud Storage |
GCPComputeInstance |
Compute Engine |
GCPCloudFunction |
Cloud Functions |
GCPCloudRunService, GCPCloudRunJob
|
Cloud Run |
GCPWorkloadIdentityPool, GCPWorkloadIdentityProvider
|
IAM Workload Identity Federation |
GCPCloudSQLInstance |
Cloud SQL |
GCPArtifactRegistryRepo |
Artifact Registry |
GCPBigQueryDataset, GCPBigQueryTable, GCPBigQueryRoutine
|
BigQuery |
GCPPubSubTopic, GCPPubSubSubscription, GCPPubSubSchema, GCPPubSubSnapshot
|
Pub/Sub |
GCPServiceDirectoryNamespace, GCPServiceDirectoryService
|
Service Directory |
GCPCloudTasksQueue |
Cloud Tasks |
GCPKmsKeyRing, GCPKmsCryptoKey, GCPKmsCryptoKeyVersion
|
KMS |
GCPSpannerInstance, GCPSpannerDatabase
|
Spanner |
GCPSecret |
Secret Manager |
GCPServiceAccountResource |
A service account modeled as a resource target — emitted only when the SA's resource name has no email (a numeric unique-id SA). An email-named SA that is the target of an edge (e.g. CAN_IMPERSONATE_SA) instead collapses onto its serviceAccount:<email> principal node, so a service account is a single node that is both actor and object. |
GoogleDriveFile |
Public / anyone-with-link Google Drive file (drive_file_<id>; from enum_drive) |
| Edge Type | Meaning | Emitted By |
|---|---|---|
HAS_IAM_BINDING |
Principal contributes to a (single) binding node | IAM binding stage |
HAS_COMBO_BINDING |
Principal contributes to a combo (GCPIamMultiBinding) node |
IAM binding multi-permission stage |
CONTRIBUTES_TO_COMBO |
A contributing simple binding feeds into a combo binding node | IAM binding multi-permission stage |
HAS_IMPLIED_PERMISSIONS |
Principal contributes to an inferred/implied-permission binding node | inferred-permissions stage |
POLICY_BINDINGS |
Generic binding-node -> target edge for non-dangerous bindings (mainly visible with --include-all) |
IAM binding stage |
ROLE_OWNER / ROLE_EDITOR
|
Collapsed high-signal role edges for roles/owner and roles/editor (collapse all matched dangerous rule edges for the same grant->target path) |
IAM binding stage |
EXISTS_IN_PROJECT |
Resource is modeled inside a project | resource expansion stage |
EXECUTES_WITH |
Compute instance executes as its attached service account | resource expansion stage |
GCP_SERVICE_ACCOUNT_KEY_FOR |
Service account key belongs to a service account | resource expansion stage |
GCP_PRINCIPAL_SET |
Service account is a member of a Cloud Resource Manager service-account principal set | principal stage |
WIF_PROVIDER_IN_POOL |
Workload Identity Provider belongs to a Workload Identity Pool | resource expansion stage |
WIF_PRINCIPAL_IN_POOL |
WIF principal/principal set belongs to a workload identity pool | resource expansion stage |
GCP_FEDERATION_POSSIBLE |
No-condition external identity source can federate into a provider | resource expansion stage |
Seeded in stage_1_principals.py from Workspace and IAM data:
| Edge Type | Meaning | Triggered by |
|---|---|---|
GOOGLE_MEMBER_OF |
Principal (member) is a member of a Google Group (group_member) |
explicit group_memberships rows |
DOMAIN_MEMBER_OF |
Principal maps into a domain:<suffix> node by email suffix |
inferred only when the destination domain node already exists |
CAN_IMPERSONATE |
A Workspace super-admin can impersonate any user in the tenant (super-admin -> every other user node) | requires enumerated workspace_admin_roles (super-admin) + workspace_role_assignments; no-op otherwise |
CAN_RESET_PASSWORD |
A Workspace super-admin can reset the password of any user | same super-admin trigger as CAN_IMPERSONATE
|
DOMAIN_WIDE_DELEG |
A service account with domain-wide delegation can impersonate a Workspace user (SA -> every user in the tenant) |
workspace_delegations (proven when a SA credential impersonates an admin); no-op otherwise |
DELEGATES_INTO |
A domain-wide-delegation service account -> the GoogleWorkspaceTenant hub |
same workspace_delegations trigger |
WORKSPACE_MEMBER |
A Workspace user -> the GoogleWorkspaceTenant hub it belongs to |
emitted alongside DOMAIN_WIDE_DELEG
|
CAN_JOIN |
A self-join-open group can be joined by the broadest allowed principal (GCPAllUsers for ANYONE_CAN_JOIN, PrincipalsInOrg for ALL_IN_DOMAIN_CAN_JOIN) -> group |
workspace_group_settings; no-op otherwise |
CAN_READ |
A public / anyone-with-link Drive file is readable by anyone (GCPAllUsers -> GoogleDriveFile) |
workspace_drive_files (from enum_drive); no-op otherwise |
Notes:
-
CAN_IMPERSONATE/CAN_RESET_PASSWORD,DOMAIN_WIDE_DELEG/DELEGATES_INTO/WORKSPACE_MEMBER,CAN_JOIN, andCAN_READare all add-only and stay absent until their source Workspace data has been enumerated, so older graphs are byte-for-byte unchanged. - Convenience members (
projectOwner/Editor/Viewer) are skipped as group-membership endpoints; they are already expanded to concrete principals upstream.
The inferred-permissions stage (stage_3_inferred_permissions.py) re-runs the dangerous-rule matcher over implied permissions and emits a parallel family. Shape:
principal
-> HAS_IMPLIED_PERMISSIONS
-> implied IAM binding node (role IMPLIED_PERMISSIONS@<scope>, kind GCPIamSimpleBinding)
-> INFERRED_<RULE_EDGE_TYPE>
-> resource
Every dangerous rule edge type has an INFERRED_<EDGE> counterpart minted by prefixing the original rule edge type with INFERRED_. Examples:
INFERRED_RESET_COMPUTE_STARTUP_SAINFERRED_START_COMPUTE_STARTUP_SAINFERRED_CREATE_AND_INVOKE_CLOUDFUNCTION_AS_SAINFERRED_UPDATE_AND_INVOKE_CLOUDFUNCTION_AS_SAINFERRED_CREATE_CLOUDSCHEDULER_JOB_AS_SAINFERRED_CAN_MODIFY_PROJECT_IAM
OpenGraph expands Cloud Resource Manager principal sets for service accounts into explicit membership edges:
principalSet://cloudresourcemanager.googleapis.com/projects/<PROJECT_NUMBER>/type/ServiceAccountprincipalSet://cloudresourcemanager.googleapis.com/folders/<FOLDER_NUMBER>/type/ServiceAccountprincipalSet://cloudresourcemanager.googleapis.com/organizations/<ORG_NUMBER>/type/ServiceAccount
Graph shape:
serviceAccount:<email>
-> GCP_PRINCIPAL_SET
-> principalSet://cloudresourcemanager.googleapis.com/.../type/ServiceAccount
Interpretation note:
-
GCP_PRINCIPAL_SETonly models principal-set membership, not permission grants by itself. The edge is emitted only when the principal set already appears in IAM members, and carriesmembership_dynamic=true. - Effective access still comes from the IAM binding path:
serviceAccount:<email> -> GCP_PRINCIPAL_SET -> principalSet://... -> HAS_IAM_BINDING -> binding -> target resource - So even when a folder/org principal set member is bound at project scope, the grant still applies to that bound project resource (not arbitrary resources in other projects).
- Membership is dynamic with hierarchy changes. If a project moves into or out of a folder or organization, the service accounts included by that principal set change as well.
Reference: Google Cloud principal identifiers
OpenGraph also expands cached IAM Workload Identity Federation resources into explicit nodes:
project
-> EXISTS_IN_PROJECT
-> workload identity pool
project
-> EXISTS_IN_PROJECT
-> workload identity provider
workload identity provider
-> WIF_PROVIDER_IN_POOL
-> workload identity pool
Interpretation note:
- This first-pass model is inventory/topology only.
- It shows that a provider exists and which pool it belongs to.
- It does not yet imply that any external identity can actually federate or that any service account impersonation path exists.
OpenGraph also enriches workload identity pool principals seen in IAM bindings:
principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/subject/<SUBJECT>principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/group/<GROUP_ID>principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/attribute.<ATTRIBUTE_NAME>/<ATTRIBUTE_VALUE>principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/*principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<PROJECT_ID>.svc.id.goog/kubernetes.serviceaccount.uid/<SERVICEACCOUNT_UID>principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<PROJECT_ID>.svc.id.goog/namespace/<NAMESPACE>principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<PROJECT_ID>.svc.id.goog/kubernetes.cluster/https://container.googleapis.com/v1/projects/<PROJECT_ID>/locations/<LOCATION>/clusters/<CLUSTER_NAME>- Legacy GKE format:
serviceAccount:<PROJECT_ID>.svc.id.goog[<NAMESPACE>/<KSA>]
Graph shape:
principal://... or principalSet://...
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
Provider nodes are also enriched with configuration metadata such as:
provider_source_kindprovider_typeissuer_uriallowed_audiencesattribute_mappingattribute_conditionhas_attribute_condition
Currently recognized provider_source_kind values in the graph:
githubgitlabterraformawsazuresamloidcunknown
For providers that have no attribute condition, OpenGraph emits a broad-trust external-source warning node and a GCP_FEDERATION_POSSIBLE edge. The external-source node kind depends on the provider classification:
GitHubNoCondition / GitLabNoCondition / TerraformNoCondition / AWSNoCondition /
AzureNoCondition / SAMLNoCondition / OIDCNoCondition / ProviderNoCondition
-> GCP_FEDERATION_POSSIBLE
-> workload identity provider
(All of these external-source nodes carry kind GCPExternalIdentitySource; ProviderNoCondition is the fallback when the source kind is unknown.)
Selector interpretation note:
-
subject,group, andattribute.NAMEselectors are only fully satisfiable when at least one provider in the pool maps the corresponding value. - For GKE-managed pools (
<PROJECT_ID>.svc.id.goog), GKE selector kinds are treated as natively supported (kubernetes.serviceaccount.name,kubernetes.serviceaccount.uid,kubernetes.namespace,kubernetes.cluster) even when external-provider mappings are not present. - OpenGraph records whether the selector appears supported by the currently discovered provider mappings with properties such as:
wif_selector_supportedwif_selector_support_statuswif_selector_support_reasonwif_supporting_provider_names
Practical provider-classification note:
- GitHub detection is based on the GitHub Actions OIDC issuer.
- GitLab detection is based on GitLab OIDC issuer strings.
- Terraform detection is based on Terraform Cloud issuer strings.
- Azure detection is based on Microsoft Entra / Azure AD issuer strings.
- Other OIDC providers fall back to
oidc. - Providers that do not match the current heuristics fall back to
unknown.
These edge kinds come from gcpwn/mappings/og_privilege_escalation_paths.json. Every dangerous edge hangs off a binding/combo node (principal -[HAS_IAM_BINDING|HAS_COMBO_BINDING]-> binding-node -[dangerous edge]-> resource), never directly off the principal.
Each maps one IAM permission to a binding-node -> target edge:
| Edge Type | Permission | Target |
|---|---|---|
CAN_MODIFY_PROJECT_IAM |
resourcemanager.projects.setIamPolicy |
project |
CAN_MODIFY_FOLDER_IAM |
resourcemanager.folders.setIamPolicy |
folder |
CAN_MODIFY_ORG_IAM |
resourcemanager.organizations.setIamPolicy |
org |
CAN_MODIFY_SA_IAM |
iam.serviceAccounts.setIamPolicy |
service account |
CAN_MODIFY_COMPUTE_INSTANCE_IAM |
compute.instances.setIamPolicy |
compute instance |
CAN_MODIFY_ClOUD_RUN_FUNCTION_IAM |
cloudfunctions.functions.setIamPolicy |
cloud function |
CAN_MODIFY_SECRET_MANAGER_SECRET_IAM |
secretmanager.secrets.setIamPolicy |
secret |
CAN_IMPERSONATE_SA |
iam.serviceAccounts.implicitDelegation |
service account |
CAN_CREATE_SA_ACCESS_TOKEN |
iam.serviceAccounts.getAccessToken |
service account |
CAN_CREATE_SA_KEY |
iam.serviceAccountKeys.create |
service account |
CAN_READ_SECRET_DATA |
secretmanager.versions.access |
secret |
Note: the edge-type spelling
CAN_MODIFY_ClOUD_RUN_FUNCTION_IAM(lowercaselinClOUD) is taken verbatim from the mapping file; it is the actual emitted kind.
When a binding satisfies dangerous rules via a broad role, the individual dangerous edges collapse to a single high-signal edge:
| Role | Collapsed edge type |
|---|---|
roles/owner |
ROLE_OWNER |
roles/editor |
ROLE_EDITOR |
The collapsed edge keeps dangerous_rule_names / dangerous_edge_types properties listing the individual rules it folded. Multi-permission workflow edge types are exempt from collapsing (see below) so owner/editor can still feed combo paths.
These are the edge_to_target kinds emitted from a combo node to the final target resource:
| Combo-target edge type | What it represents |
|---|---|
RESET_COMPUTE_STARTUP_SA |
rewrite startup-script metadata + reset a RUNNING instance, paired with iam.serviceAccounts.actAs -> execute as target SA |
START_COMPUTE_STARTUP_SA |
rewrite startup-script metadata + start a STOPPED instance, paired with actAs -> execute as target SA |
CREATE_AND_INVOKE_CLOUDFUNCTION_AS_SA |
create + invoke a Cloud Function with an attached SA -> retrieve SA token |
UPDATE_AND_INVOKE_CLOUDFUNCTION_AS_SA |
update + invoke a Cloud Function -> retrieve the existing/attached SA token |
CREATE_CLOUDSCHEDULER_JOB_AS_SA |
create a Cloud Scheduler job with an attached SA via actAs -> execute as that SA |
For combo rules with a combo_hop, the path runs combo-node -(edge_from_subject)-> capability/resource node -(edge_to_target)-> target. The edge_from_subject (hop) edge kinds are:
| Hop edge type | Hop node mode | Combo it belongs to |
|---|---|---|
CAN_RESET_COMPUTE_WITH_STARTUP_SCRIPT |
resource | RESET_COMPUTE_STARTUP_SA |
CAN_START_COMPUTE_WITH_STARTUP_SCRIPT |
resource | START_COMPUTE_STARTUP_SA |
CAN_CREATE_DEPLOY_INVOKE_CLOUDFUNCTION |
capability (GCPIamCapability) |
CREATE_AND_INVOKE_CLOUDFUNCTION_AS_SA |
CAN_UPDATE_DEPLOY_INVOKE_CLOUDFUNCTION |
capability (GCPIamCapability) |
UPDATE_AND_INVOKE_CLOUDFUNCTION_AS_SA |
CAN_CREATE_CLOUDSCHEDULER_JOB |
capability (GCPIamCapability) |
CREATE_CLOUDSCHEDULER_JOB_AS_SA |
These hop edge types (and the combo-target edge types) are in the no-collapse set, so a roles/owner/roles/editor grant that matches them is NOT collapsed away into ROLE_OWNER/ROLE_EDITOR and remains visible in the combo path.
For rule definitions and customization workflow, see OpenGraph - Add Your Own Content.
-
IAM conditions are pass-through. A conditioned binding gets a deterministic node-id suffix
#cond:<10-hex-hash>and carriesconditional=true,condition_expr_raw, andcondition_hash, but the CEL expression is not evaluated. The same expression always yields the same hash (dedup); distinct expressions yield distinct nodes.--cond-evaldoes not yet narrow resources, so output is identical with it on or off. -
Inheritance fan-out. With
--expand-inherited, an ancestor (org/folder) binding is fanned down to every descendant scope; each descendant binding node id gets a@<scope>#src:<source>provenance suffix and its own dangerous edges. Without it, the ancestor binding stays attached to the ancestor only. A binding that is both inherited and conditioned carries both suffixes:...#src:<source>#cond:<hash>.
Because some edge kinds are rule-driven and some node types are data-driven, the exact set can vary by environment and flags.
Use SQL to enumerate what your current workspace actually emitted:
data sql --db service "SELECT DISTINCT node_type FROM opengraph_nodes ORDER BY node_type;"
data sql --db service "SELECT DISTINCT edge_type FROM opengraph_edges ORDER BY edge_type;"- Authentication Reference
- Workspace Instructions
- Google Workspace Enumeration
- CLI Module Reference
- Enumeration Module Reference
- Exploit Module Reference
- Downloads to Disk
- Logging & Verbosity
- Data View/Export
- IAM Enumeration and Analysis Workflow
- Troubleshooting and FAQ