-
Notifications
You must be signed in to change notification settings - Fork 28
OpenGraph Overview and Usage
If you are new to GCPwn, read Getting Started first.
Use this section to build BloodHound-compatible OpenGraph output from enumerated GCP data.
TLDR module split:
-
process_og_gcpwn_databuilds and exports the OpenGraph JSON. -
process_og_node_color_imagesupdates BloodHound custom node colors/icons.
- Graphing Strategy
- Importing Data (Data Sources)
- Main Command
- Build From a Cloud Asset Inventory Export File
- OpenGraph Node Customize TLDR
- Core Flags
- Step-Selective Flags
- Cross-Project Service Account Usage
- OpenGraph Reference Pages
You might notice edges go to role@location instead of going directly to the project. This preserves authorization fidelity in the graph.
If User A has compute.admin on Project A and User B has storage.admin on Project A, drawing both users directly to Project A and then Project A to all resources would incorrectly imply both users can reach the same resources, when User A can only get to compute and User B can only get to storage.
The correct model is to route each user through their specific role binding node at that location, and only then fan out to resources that role can actually affect.
Incorrect method (over-broad reach):
User A --> Project A --> Compute & Storage
User B --> Project A --> Compute & Storage
Correct method (binding-scoped reach):
User A --> compute_admin@project:A --> Compute Resources in Project A
User B --> storage_admin@project:A --> Storage Resources in Project A
If Google Workspace data has been enumerated (the workspace_* tables populated by the Workspace enum modules — Cloud Identity users/groups, Directory admin roles/role assignments, etc.), the users/groups stage seeds high-confidence identity nodes before any IAM-derived principals are added:
-
workspace_usersrows becomeGoogleUsernodes. -
workspace_groupsrows becomeGoogleGroupnodes. -
group_membershipsrows becomemember -[GOOGLE_MEMBER_OF]-> groupedges.
Seeding Workspace principals first means IAM-derived principals merge into these richer user/group nodes instead of staying generic GCPPrincipal objects.
A Workspace super-admin can impersonate, and reset the password of, any user in the tenant. When super-admin role data is present (workspace_admin_roles flagged super-admin + workspace_role_assignments), the stage emits, from each super-admin to every other Workspace user:
super-admin user -> CAN_IMPERSONATE -> every other workspace user
super-admin user -> CAN_RESET_PASSWORD -> every other workspace user
These two edges are add-only: with no super-admin role data enumerated they are a no-op, so graphs built before Workspace admin-role enumeration are byte-for-byte unchanged. They matter as an attack path because a super-admin can take over any Workspace user, and that user may in turn hold GCP IAM (super-admin -> user -> GCP resource). See the OpenGraph - Node/Edge Tables for the full identity/membership edge list.
Service-account principal sets are also expanded in resource expansion mode:
serviceAccount:<email>
-> GCP_PRINCIPAL_SET
-> principalSet://cloudresourcemanager.googleapis.com/projects/<PROJECT_NUMBER>/type/ServiceAccount
Equivalent principal-set membership expansion is emitted for folder and organization scopes as well.
This expansion is a current-membership snapshot based on the discovered resource hierarchy in the workspace. If projects move into or out of folders or organizations, the emitted principal-set membership will change on the next rebuild.
IAM Workload Identity Pools and Providers are also expanded in resource expansion mode:
project -> EXISTS_IN_PROJECT -> workload identity pool
project -> EXISTS_IN_PROJECT -> workload identity provider
workload identity provider -> WIF_PROVIDER_IN_POOL -> workload identity pool
This is currently an inventory/topology layer. It helps blue teams see which pools and providers exist and how they are related before deeper federation-path logic is added.
If IAM bindings reference workload identity pool principals, OpenGraph also links those principal members back to their pool:
principal://.../subject/<value>
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
principalSet://.../group/<value>
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
principalSet://.../attribute.<name>/<value>
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
principalSet://.../*
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
principal://.../kubernetes.serviceaccount.uid/<uid>
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
principalSet://.../namespace/<namespace>
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
principalSet://.../kubernetes.cluster/https://container.googleapis.com/v1/projects/.../locations/.../clusters/...
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
serviceAccount:<project_id>.svc.id.goog[<namespace>/<ksa>] (legacy GKE format)
-> WIF_PRINCIPAL_IN_POOL
-> workload identity pool
Provider nodes are also enriched with attribute mappings and attribute conditions. If a provider has no attribute condition, OpenGraph emits a broad-trust source node such as GitHubNoCondition or GitLabNoCondition:
GitHubNoCondition -> GCP_FEDERATION_POSSIBLE -> workload identity provider
Currently recognized provider/source kinds in the graph are:
githubgitlabterraformawsazuresamloidcunknown
For subject, group, and attribute.NAME WIF principal selectors, OpenGraph also checks whether the currently discovered providers in that pool expose the needed mapping. This helps distinguish:
- selectors that appear usable from provider configuration
- selectors that are present in IAM but currently have no matching provider mapping
For GKE-managed pools (<PROJECT_ID>.svc.id.goog), selector kinds for Kubernetes identities are handled as native GKE selectors and treated as supported without requiring external provider mappings.
Basic-role example:
Alice --> roles/owner@project:A --> ROLE_OWNER --> Target Resources
Rather than emitting a long list of separate CAN_* edges for that same owner binding, default mode collapses those into ROLE_OWNER and preserves matched edge detail in edge properties.
OpenGraph turns tabular facts about your target (the resource hierarchy, IAM bindings,
service accounts, etc.) into a BloodHound graph. Those facts come from one of two sources —
the resulting graph is identical either way, because both populate the same tables
(iam_allow_policies, abstract_tree_hierarchy, iam_service_accounts, service-account keys,
cloudcompute_instances, and Workload Identity pools/providers).
Source 1 — GCPwn's enumerated tables (default). Run enumeration first; then
process_og_gcpwn_data reads the workspace's SQLite tables with no extra flag (and persists
the graph back to the workspace opengraph_* tables). For the full graph run enum_all; for
just the IAM attack graph you only need the identity + binding data:
modules run enum_all # everything, or:
modules run enum_all --modules iam,resource-manager # SAs, roles, hierarchy
modules run enum_gcp_policy_bindings # IAM bindings across services
modules run process_og_gcpwn_data --expand-inherited --out Bloodhound_OutputSource 2 — a Cloud Asset Inventory (CAI) export. CAI returns resources + IAM policies for a whole org/folder/project in one pull, often with lighter permissions than per-service enumeration. Get a CAI export three ways, then graph it:
| How you get CAI | Command(s) | Notes |
|---|---|---|
| In GCPwn (into the native tables) |
modules run enum_asset_inventory --scope organizations/ORG_ID (or folders/ID / projects/ID), then graph exactly as Source 1 — or in one pass modules run enum_all --asset-inventory (opt-in) |
Populates the same tables; graph persists to the workspace |
| gcloud SDK (to a file) |
gcloud asset export --organization ORG_ID --content-type resource --output-path gs://BUCKET/assets.json (repeat with --content-type iam-policy), download the NDJSON, then process_og_gcpwn_data --cai-file assets.json
|
Builds straight from the file — no enum, no DB. See Build From a CAI Export File |
| Cloud Console UI | Console → Asset Inventory → Export → select Resource (and IAM Policy) content types → export to a GCS bucket (or BigQuery), download the NDJSON, then --cai-file it |
Same as the SDK path |
The in-GCPwn
enum_asset_inventorypath is opt-in insideenum_all(--asset-inventory) and writes to the shared tables, so afterwards OpenGraph behaves exactly like Source 1. The--cai-filepath is standalone (export-only, no workspace DB) — see the detailed section below.
modules run process_og_gcpwn_data --expand-inherited --reset --out Bloodhound_Output.jsonExample output:
[*] Step 1 [1]: users_groups (Users/Groups graph)
[*] Completed users_groups: +97 nodes, +0 edges
[*] Step 2 [2]: iam_bindings (IAM policy bindings graph)
[*] Completed iam_bindings: +124 nodes, +300 edges
[*] Step 3 [3]: inferred_permissions (Inferred permissions graph)
[*] Completed inferred_permissions: +2 nodes, +2 edges
[*] Step 4 [4]: resource_expansion (Resource expansion graph)
[*] Completed resource_expansion: +57 nodes, +57 edges
[*] Pruned isolated service-account IAM-binding islands (pairs=14, key_islands=9, nodes=49, edges=26).
[*] Pruned orphan implied-IAM-binding nodes (implied_bindings=2, nodes=2, edges=2).
[*] Pruned isolated service-account nodes (service_accounts=43, nodes=43, edges=0).
[*] OpenGraph generation complete. Nodes: 185 | Edges: 328
[*] Saved graph JSON to Bloodhound_Output.json
process_og_gcpwn_data --cai-file <export> builds the BloodHound OpenGraph graph directly from a Cloud Asset Inventory (CAI) export file instead of the cached SQLite tables. This means you can graph an environment with no prior enumeration and no GCPwn workspace data — you only need a CAI export to read from.
The export file can be:
- NDJSON (one asset per line), as produced by
gcloud asset export/exportAssets, or - a JSON array of assets.
Internally a CaiFileSource maps CAI asset types into the same tables the OpenGraph pipeline normally reads, and the unchanged pipeline runs over them. Both RESOURCE and IAM_POLICY content types are useful here (resource topology + IAM bindings).
Important behavior:
- The generated graph is exported to JSON but is NOT persisted to the current workspace's
opengraph_nodes/opengraph_edgestables. It is export-only. -
--cai-filecannot be combined with--use-existing-opengraph-db. - Pair it with
-o/--outto choose the output directory for the exported JSON (and any split outputs).
# Produce a CAI export with gcloud (RESOURCE + IAM_POLICY), then graph it:
gcloud asset export \
--project my-project \
--content-type resource \
--output-path "gs://my-bucket/cai_resource.json"
# (download the export locally, then build the graph from it)
modules run process_og_gcpwn_data --cai-file cai_resource.json --out cai_graph_outputExample output:
[*] Building OpenGraph from CAI export cai_resource.json (1240 assets -> abstract_tree_hierarchy=12, cloudcompute_instances=37, iam_allow_policies=88, ...).
[*] Step 1 [1]: users_groups (Users/Groups graph)
...
[*] CAI-file mode: skipping persist to workspace opengraph tables (export only).
[*] OpenGraph generation complete. Nodes: 185 | Edges: 328
[*] Saved graph JSON to cai_graph_output/opengraph_cai_graph_output.json
The same stage-selective and IAM-behavior flags (--expand-inherited, --include-all, --cond-eval, the step-selective flags below) apply in CAI-file mode.
Direct flags:
modules run process_og_node_color_images \
--push-custom-node-attributes-url http://127.0.0.1:8080/api/v2/custom-nodes \
--custom-node-auth-mode signature \
--push-custom-node-attributes-token-id <TOKEN_ID> \
--push-custom-node-attributes-token-key <TOKEN_KEY>Example output:
[*] custom-nodes sync complete: unchanged=42, updated=3, created=1
[*] Saved BloodHound signature credentials to opengraph_ui_config for optional future reuse.
Interactive walkthrough:
(None:None)> modules run process_og_node_color_images
[*] No arguments supplied. Launching BloodHound custom-node sync setup.
[*] BloodHound custom-node URL (press Enter to use default: http://127.0.0.1:8080/api/v2/custom-nodes):
> Custom-node sync: choose auth mode
>> [1] Bearer JWT
>> [2] API key signature
> [3] Exit
> Choose an option: 2
> Choose a saved BloodHound API token or enter a new one:
>> [1] Saved Token ID: <TOKEN_ID> (http://127.0.0.1:8080/api/v2/custom-nodes)
>> [2] Enter a new API token ID/key
> [3] Exit
> Choose an option: 1
[*] custom-nodes sync complete: unchanged=46, updated=0, created=0
| Flag | Purpose |
|---|---|
-o / --out <dir>
|
Output directory for exported OpenGraph JSON (files written as opengraph_<folder_name>.json plus any split outputs) |
--cai-file <export> |
Build the graph directly from a Cloud Asset Inventory export file (NDJSON or JSON array) instead of SQLite; no prior enum/DB needed, export-only (not persisted). See Build From a Cloud Asset Inventory Export File |
--reset |
Clear existing OpenGraph rows before rebuild |
--use-existing-opengraph-db |
Skip the rebuild and re-export from already-persisted opengraph_nodes/opengraph_edges
|
--include-all |
Keep broader IAM-derived edges (more complete, noisier output) |
--expand-inherited |
Expand inherited IAM scope fan-out across org/folder/project/resource hierarchy |
--cross-sa-project-allowed [PROJECT_ID ...] |
Model attack paths where the actAs SA lives in a different project than the compute resource (Cloud Run/Build/Scheduler). Off by default. Pass all to enable globally, or specific project IDs to limit scope. See Cross-Project Service Account Usage
|
--deny-policies |
Factor IAM v2 Deny Policies into the graph as a final filter — allow minus deny. Requires enum_gcp_policy_bindings to have collected deny policies |
--exclude-service-agents |
Drop service agent nodes and their edges from the output before persistence |
--cond-eval |
Conditional evaluation mode (currently pass-through behavior) |
If no step-selective flags are passed, all stages run.
| Flag | What It Runs |
|---|---|
--groups |
Users/groups mapping stage |
--iam-bindings |
IAM bindings stage |
--inferred-permissions |
Inferred-permissions stage |
--resource-expansion |
Resource-expansion stage |
By default, GCPwn's OpenGraph only draws actAs-based attack paths (Cloud Run, Cloud Build, Cloud Scheduler) when the target service account is in the same project as the resource being created. This matches GCP's default constraint: iam.disableCrossProjectServiceAccountUsage is enforced at the project level, and most projects block it.
If that constraint has been disabled in a project, a principal with iam.serviceAccounts.actAs on a SA in project B can create a Cloud Run service in project A pinned to that SA. GCPwn will miss those paths unless you tell it cross-project usage is in play.
# Allow cross-project paths for specific projects (recommended — avoids false positives)
modules run process_og_gcpwn_data --cross-sa-project-allowed proj-a proj-b
# Allow cross-project paths for every project in the workspace (use only if you know the constraint is off everywhere)
modules run process_og_gcpwn_data --cross-sa-project-allowed all
# or equivalently:
modules run process_og_gcpwn_data --cross-sa-project-allowedPass the project IDs of the resource-hosting projects (the ones where Cloud Run / Cloud Build / Scheduler jobs are created), not the SA's home project.
Run this against each project you want to verify:
gcloud resource-manager org-policies describe \
iam.disableCrossProjectServiceAccountUsage \
--project PROJECT_ID \
--format="value(booleanPolicy.enforced)"| Output | Meaning |
|---|---|
True |
Constraint is enforced — cross-project SA usage is blocked (default). Do NOT pass this project to --cross-sa-project-allowed. |
False |
Constraint is disabled — cross-project SA usage is allowed. Add this project ID to --cross-sa-project-allowed. |
| (empty / not found) | No project-level override; the constraint inherits from the folder or org. Check the parent. |
To check the org-level default:
gcloud resource-manager org-policies describe \
iam.disableCrossProjectServiceAccountUsage \
--organization ORG_ID \
--format="value(booleanPolicy.enforced)"Your workspace covers proj-app (hosts Cloud Run services) and proj-shared-sa (hosts service accounts). You suspect proj-app has the constraint disabled:
gcloud resource-manager org-policies describe \
iam.disableCrossProjectServiceAccountUsage \
--project proj-app \
--format="value(booleanPolicy.enforced)"
# Returns: False <-- cross-project SA usage is allowed in proj-appRe-run OpenGraph with that project flagged:
modules run process_og_gcpwn_data --cross-sa-project-allowed proj-appGCPwn will now include paths like:
attacker -> actAs -> sa@proj-shared-sa -> CREATE_CLOUDRUN_SERVICE_AS_SA -> proj-app
that would otherwise be hidden.
- 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