Skip to content

OpenGraph Node Edge Tables

WebbinRoot edited this page Jul 10, 2026 · 4 revisions

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. The principal_type() / gcp_resource_node_type() helpers in core_helpers.py decide identity/resource node kinds; the dangerous-rule edge kinds come from og_privilege_escalation_paths.json via iam_bindings_shared_helpers.py.

Table of Contents

Node Types

Identity / principal node types

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

Resource and IAM-structure node types

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

Service-specific resource node types

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 Types

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

Identity / Membership Edges

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, and CAN_READ are 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.

Inferred Permission Edges

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_SA
  • INFERRED_START_COMPUTE_STARTUP_SA
  • INFERRED_CREATE_AND_INVOKE_CLOUDFUNCTION_AS_SA
  • INFERRED_UPDATE_AND_INVOKE_CLOUDFUNCTION_AS_SA
  • INFERRED_CREATE_CLOUDSCHEDULER_JOB_AS_SA
  • INFERRED_CAN_MODIFY_PROJECT_IAM

Cloud Resource Manager ServiceAccount Principal Sets

OpenGraph expands Cloud Resource Manager principal sets for service accounts into explicit membership edges:

  • principalSet://cloudresourcemanager.googleapis.com/projects/<PROJECT_NUMBER>/type/ServiceAccount
  • principalSet://cloudresourcemanager.googleapis.com/folders/<FOLDER_NUMBER>/type/ServiceAccount
  • principalSet://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_SET only models principal-set membership, not permission grants by itself. The edge is emitted only when the principal set already appears in IAM members, and carries membership_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

Workload Identity Pools and Providers

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_kind
  • provider_type
  • issuer_uri
  • allowed_audiences
  • attribute_mapping
  • attribute_condition
  • has_attribute_condition

Currently recognized provider_source_kind values in the graph:

  • github
  • gitlab
  • terraform
  • aws
  • azure
  • saml
  • oidc
  • unknown

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, and attribute.NAME selectors 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_supported
    • wif_selector_support_status
    • wif_selector_support_reason
    • wif_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.

Dangerous Rule Edges

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.

Default single-permission rule edges

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 (lowercase l in ClOUD) is taken verbatim from the mapping file; it is the actual emitted kind.

Collapsed role edges

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.

Default multi-permission rule edges (combo targets)

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

Capability-hop edges

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.

Conditions and Inheritance (effect on emitted nodes/edges)

  • IAM conditions are pass-through. A conditioned binding gets a deterministic node-id suffix #cond:<10-hex-hash> and carries conditional=true, condition_expr_raw, and condition_hash, but the CEL expression is not evaluated. The same expression always yields the same hash (dedup); distinct expressions yield distinct nodes. --cond-eval does 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>.

Get Exact Types from Your Run

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;"

Clone this wiki locally