mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-08 08:03:50 -10:00
Compare commits
207
Commits
codeowners
...
v2.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b0925877d | ||
|
|
7727d1d08c | ||
|
|
c7f81dc5cc | ||
|
|
008fb84d23 | ||
|
|
142757b5c2 | ||
|
|
146034d619 | ||
|
|
9e056b29d7 | ||
|
|
871491738b | ||
|
|
033c953d57 | ||
|
|
a7e45b9ae5 | ||
|
|
ce513ba04d | ||
|
|
83f9af249d | ||
|
|
11652a9a6e | ||
|
|
6ac7a0bf6b | ||
|
|
c6fcdba665 | ||
|
|
4f2c2cb4c9 | ||
|
|
db8a6895c3 | ||
|
|
11763f04f6 | ||
|
|
0ba9e2d37f | ||
|
|
a3d1c09558 | ||
|
|
a3b7ab4269 | ||
|
|
60c6b8d61c | ||
|
|
8db014b136 | ||
|
|
72cd367086 | ||
|
|
f2d4b7c535 | ||
|
|
47d69d870f | ||
|
|
c9d1c2de68 | ||
|
|
a8e802404f | ||
|
|
d021ea54de | ||
|
|
7a18090609 | ||
|
|
cb8be678e7 | ||
|
|
6f45cbe900 | ||
|
|
8a248e3127 | ||
|
|
de141300e9 | ||
|
|
eb76d76dea | ||
|
|
72a551b38b | ||
|
|
59bac4ee36 | ||
|
|
44bf8e8915 | ||
|
|
70107c607f | ||
|
|
d24f817bf9 | ||
|
|
711e29e3bb | ||
|
|
04bb167273 | ||
|
|
ed732d0c01 | ||
|
|
f82547b438 | ||
|
|
35fca31475 | ||
|
|
888649616f | ||
|
|
2b272a1335 | ||
|
|
c94996e470 | ||
|
|
2f80731e5b | ||
|
|
d473e31c06 | ||
|
|
baf62fdbb2 | ||
|
|
f3f98f24e3 | ||
|
|
eedf397c93 | ||
|
|
31e2fe5fcb | ||
|
|
06a9dabf48 | ||
|
|
ce545ca9ff | ||
|
|
8954fc2a72 | ||
|
|
7bb7d947ef | ||
|
|
3c1213b0ff | ||
|
|
fbc862aef1 | ||
|
|
8dddcc83d2 | ||
|
|
36b45a64c5 | ||
|
|
333ca05a11 | ||
|
|
4a2444c05f | ||
|
|
c9edf8450b | ||
|
|
ec9a50ead1 | ||
|
|
eae16c32b4 | ||
|
|
0df2aa9240 | ||
|
|
abf1543b3e | ||
|
|
69c9070009 | ||
|
|
8f6208d983 | ||
|
|
83d9611732 | ||
|
|
b994b42708 | ||
|
|
52c12855a1 | ||
|
|
70037750c5 | ||
|
|
1bb8576a27 | ||
|
|
16b3c66624 | ||
|
|
3dbb98ffc6 | ||
|
|
b3a03ec7bd | ||
|
|
521cf7e4f4 | ||
|
|
5115f849f5 | ||
|
|
9e42b53d22 | ||
|
|
b28ed6a3fd | ||
|
|
0e7b006cde | ||
|
|
2cc97f508b | ||
|
|
031d666c63 | ||
|
|
d0de59dbe4 | ||
|
|
00b49aa33c | ||
|
|
12ee6c292a | ||
|
|
4584774ec1 | ||
|
|
d558e1cf55 | ||
|
|
19b825cfa6 | ||
|
|
698458cf92 | ||
|
|
dfed0491f2 | ||
|
|
7671e7e717 | ||
|
|
0c858e6cd4 | ||
|
|
0deb568e41 | ||
|
|
2806f7ea08 | ||
|
|
e54ffe8ce7 | ||
|
|
56e64d8e62 | ||
|
|
43fe99e832 | ||
|
|
5d2f2616af | ||
|
|
d8e4157e93 | ||
|
|
a104639354 | ||
|
|
296710c474 | ||
|
|
278e2658ee | ||
|
|
6b45f54699 | ||
|
|
2391bb7802 | ||
|
|
68d68fc75d | ||
|
|
7d6d16a4f1 | ||
|
|
caaf399e9d | ||
|
|
1f4f128a7a | ||
|
|
16c0f4dfe9 | ||
|
|
7e6439924a | ||
|
|
d3c042c1e3 | ||
|
|
cda06fff7a | ||
|
|
e49402991a | ||
|
|
cae31fbf21 | ||
|
|
4e75b1151b | ||
|
|
831198c42d | ||
|
|
2cb5ad2b83 | ||
|
|
efe95c02ee | ||
|
|
13bd04d3e4 | ||
|
|
b8d9d8093c | ||
|
|
b139513244 | ||
|
|
f058e1c57d | ||
|
|
a94bdc1853 | ||
|
|
89b3492399 | ||
|
|
67b84f0c74 | ||
|
|
8eec1c331b | ||
|
|
5e0eb15f7d | ||
|
|
ec497faba4 | ||
|
|
ed8e0a229c | ||
|
|
b60f479c2d | ||
|
|
ab4866fcbf | ||
|
|
00fe4fdce3 | ||
|
|
f7d1cbcdb6 | ||
|
|
b3c25bb5d0 | ||
|
|
9076beb111 | ||
|
|
878d7ec0c4 | ||
|
|
ebf9985d40 | ||
|
|
8e1e8d0c0a | ||
|
|
a88e98aedd | ||
|
|
85b9b11798 | ||
|
|
6b0970ebd7 | ||
|
|
e0dc71b414 | ||
|
|
b31f36746b | ||
|
|
0a95240b7f | ||
|
|
785b69316a | ||
|
|
547c48a249 | ||
|
|
aed3c0abdb | ||
|
|
0184e1520d | ||
|
|
70a050a973 | ||
|
|
738085dc78 | ||
|
|
0ac24e3aa1 | ||
|
|
5ba8b087ff | ||
|
|
b02ad612f0 | ||
|
|
3759d2c57d | ||
|
|
0b88bcac56 | ||
|
|
91f5f14f0e | ||
|
|
ca810a00d7 | ||
|
|
d9cbefd911 | ||
|
|
a8d2bfbfb3 | ||
|
|
bfa95acafe | ||
|
|
c0e4301ef3 | ||
|
|
22417f174b | ||
|
|
c3be95b3db | ||
|
|
abcc930f20 | ||
|
|
77cd08b8d5 | ||
|
|
57194ef3e0 | ||
|
|
82635fd44f | ||
|
|
67af1bd7fb | ||
|
|
79f3899ead | ||
|
|
8ab4c4200a | ||
|
|
6e52e5ee7b | ||
|
|
9920c411c4 | ||
|
|
356d57b2de | ||
|
|
3a45583015 | ||
|
|
57832ada7c | ||
|
|
608f07804e | ||
|
|
de492fa6e9 | ||
|
|
590a94b576 | ||
|
|
fca22ceb6b | ||
|
|
4ef87d8b98 | ||
|
|
8fac75ed11 | ||
|
|
3ff704e98f | ||
|
|
05fd899219 | ||
|
|
36c9a5b795 | ||
|
|
dd6dd291c5 | ||
|
|
42b0f3a212 | ||
|
|
e4e548012f | ||
|
|
24b7076bc3 | ||
|
|
e4f3421ea1 | ||
|
|
e61a401733 | ||
|
|
a13527a7a9 | ||
|
|
aeccb6db6d | ||
|
|
4d3c6a4c67 | ||
|
|
ac1125d47e | ||
|
|
009321e7cb | ||
|
|
4cf58a3479 | ||
|
|
f5437b7afb | ||
|
|
ef25d8b04d | ||
|
|
1e2eaf5576 | ||
|
|
66ffc57438 | ||
|
|
c4a276e4be | ||
|
|
4edbd3326c | ||
|
|
5c40ef45df |
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"
|
||||
+8
-1
@@ -62,8 +62,11 @@ terraform.rc
|
||||
.DS_Store
|
||||
|
||||
untracked/*
|
||||
*tmp*
|
||||
tmp/*
|
||||
output/*
|
||||
*cloudfox-output*
|
||||
cloudfox-*
|
||||
cloudfox
|
||||
*.log
|
||||
*.bak
|
||||
@@ -71,4 +74,8 @@ cloudfox
|
||||
*.json
|
||||
*.csv
|
||||
*.log
|
||||
dist/
|
||||
dist/
|
||||
|
||||
# graphvis files
|
||||
*.gv
|
||||
*.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 | 33 |
|
||||
| Azure | 4 |
|
||||
| GCP | Support Planned |
|
||||
| AWS | 34 |
|
||||
| Azure | 4 |
|
||||
| GCP | 60 |
|
||||
| Kubernetes | Support Planned |
|
||||
|
||||
|
||||
@@ -102,7 +106,53 @@ Additional policy notes (as of 09/2022):
|
||||
| `arn:aws:iam::aws:policy/AdministratorAccess` | This will work just fine with CloudFox, but if you were handed this level of access as a penetration tester, that should probably be a finding in itself :) |
|
||||
|
||||
### Azure
|
||||
* Viewer or similar permissions applied.
|
||||
* Viewer or similar permissions applied.
|
||||
|
||||
### GCP
|
||||
* Google Cloud SDK installed and authenticated
|
||||
* Application Default Credentials configured (`gcloud auth application-default login`)
|
||||
* Recommended permissions at appropriate hierarchy levels (see below)
|
||||
|
||||
#### GCP Permissions: Minimal vs Comprehensive
|
||||
|
||||
**Minimal Permissions (Single Project):**
|
||||
|
||||
For basic enumeration of a single project, the `roles/viewer` role provides read access to most resources (includes logging, monitoring, and compute/network viewing).
|
||||
|
||||
**Comprehensive Permissions (Organization-Wide):**
|
||||
|
||||
For thorough security assessments across an entire organization:
|
||||
|
||||
| Scope | Role | Purpose |
|
||||
| - | - | - |
|
||||
| **Organization** | `roles/resourcemanager.organizationViewer` | View organization structure and metadata |
|
||||
| **Organization** | `roles/iam.securityReviewer` | Review IAM policies across the organization |
|
||||
| **Organization** | `roles/cloudasset.viewer` | Query Cloud Asset Inventory for all resources |
|
||||
| **Organization** | `roles/cloudidentity.groupsViewer` | Enumerate Google Groups and memberships |
|
||||
| **Folder** | `roles/resourcemanager.folderViewer` | View folder hierarchy and metadata |
|
||||
| **Project** | `roles/viewer` | Read access to most project resources (includes logging.viewer, monitoring.viewer, compute.viewer) |
|
||||
| **Tooling Project** | `roles/serviceusage.serviceUsageAdmin` | (Optional) Manage API quotas for CloudFox operations |
|
||||
|
||||
> **Note:** The basic `roles/viewer` role includes permissions from `roles/logging.viewer`, `roles/monitoring.viewer`, and `roles/compute.networkViewer`, so these don't need to be granted separately.
|
||||
|
||||
#### GCP API Requirements
|
||||
|
||||
**APIs must be enabled in each project you want to assess.** GCP APIs are project-scoped.
|
||||
|
||||
| API | Service Name | Purpose |
|
||||
| - | - | - |
|
||||
| Cloud Identity API | `cloudidentity.googleapis.com` | Group enumeration, inherited role analysis |
|
||||
| Cloud Asset API | `cloudasset.googleapis.com` | Cross-project resource discovery |
|
||||
| Cloud Resource Manager API | `cloudresourcemanager.googleapis.com` | Organization mapping, IAM enumeration |
|
||||
| IAM API | `iam.googleapis.com` | IAM analysis, privilege escalation detection |
|
||||
| Compute Engine API | `compute.googleapis.com` | Instance enumeration, network security |
|
||||
| Secret Manager API | `secretmanager.googleapis.com` | Secrets enumeration |
|
||||
| Cloud Functions API | `cloudfunctions.googleapis.com` | Serverless enumeration |
|
||||
| Cloud Run API | `run.googleapis.com` | Serverless enumeration |
|
||||
| Kubernetes Engine API | `container.googleapis.com` | Container security analysis |
|
||||
| BigQuery API | `bigquery.googleapis.com` | Data security analysis |
|
||||
|
||||
For detailed setup instructions, see the [GCP Setup Guide](https://github.com/BishopFox/cloudfox/wiki/GCP-Setup-Guide).
|
||||
|
||||
# AWS Commands
|
||||
| Provider | Command Name | Description
|
||||
@@ -111,6 +161,7 @@ Additional policy notes (as of 09/2022):
|
||||
| AWS | [access-keys](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#access-keys) | Lists active access keys for all users. Useful for cross referencing a key you found with which in-scope account it belongs to. |
|
||||
| AWS | [api-gw](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#api-gw) | Lists API gateway endpoints and gives you custom curl commands including API tokens if they are stored in metadata. |
|
||||
| AWS | [buckets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#filesystems) | Lists the buckets in the account and gives you handy commands for inspecting them further. |
|
||||
| AWS | [cape](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#cape) | Enumerates cross-account privilege escalation paths. Requires `pmapper` to be run first |
|
||||
| AWS | [cloudformation](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#cloudformation) | Lists the cloudformation stacks in the account. Generates loot file with stack details, stack parameters, and stack output - look for secrets. |
|
||||
| AWS | [codebuild](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#codebuild) | Enumerate CodeBuild projects |
|
||||
| AWS | [databases](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#databases) | Enumerate RDS databases. Get a loot file with connection strings. |
|
||||
@@ -132,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`. |
|
||||
@@ -140,6 +191,7 @@ Additional policy notes (as of 09/2022):
|
||||
| AWS | [sqs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sqs) | This command enumerates all of the sqs queues and gives you the commands to receive messages from a queue and send messages to a queue (if you have the permissions needed). This command also attempts to summarize queue resource policies if they exist.|
|
||||
| AWS | [tags](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#tags) | List all resources with tags, and all of the tags. This can be used similar to inventory as another method to identify what types of resources exist in an account. |
|
||||
| AWS | [workloads](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#workloads) | List all of the compute workloads and what role they have. Tells you if any of the roles are admin (bad) and if you have pmapper data locally, it will tell you if any of the roles can privesc to admin (also bad) |
|
||||
| AWS | [ds](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#workloads) | List all of the AWS-managed directories and their attributes. Also summarizes the current trusts with their directions and types. |
|
||||
|
||||
|
||||
# Azure Commands
|
||||
@@ -151,9 +203,84 @@ Additional policy notes (as of 09/2022):
|
||||
| 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 |
|
||||
|
||||
|
||||
# GCP Commands
|
||||
|
||||
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 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)
|
||||
|
||||
+538
@@ -0,0 +1,538 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
titleStyle = func() lipgloss.Style {
|
||||
b := lipgloss.RoundedBorder()
|
||||
b.Right = "├"
|
||||
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
|
||||
}()
|
||||
|
||||
infoStyle = func() lipgloss.Style {
|
||||
b := lipgloss.RoundedBorder()
|
||||
b.Left = "┤"
|
||||
return titleStyle.Copy().BorderStyle(b)
|
||||
}()
|
||||
magentaStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("magenta"))
|
||||
)
|
||||
|
||||
type model struct {
|
||||
preloadedData *AllAccountData
|
||||
awsAccountsTable table.Model
|
||||
awsAccountsViewport viewport.Model
|
||||
|
||||
mainTable table.Model
|
||||
mainTableViewport viewport.Model
|
||||
mainTableData map[int][]table.Row
|
||||
|
||||
detailsData map[int]string
|
||||
detailsViewport viewport.Model // Use viewport for details view
|
||||
|
||||
focusSelector int
|
||||
defaultTableStyle table.Styles
|
||||
|
||||
terminalWidth int
|
||||
terminalHeight int // Add terminal height to manage viewport size
|
||||
awsSelectedRow int
|
||||
mainSelectedRow int // Track the currently selected row for detail view updates
|
||||
keys keyMap
|
||||
help help.Model
|
||||
lastKey string
|
||||
quitting bool
|
||||
}
|
||||
|
||||
type CapeJSON struct {
|
||||
Account string `json:"account"`
|
||||
Source string `json:"source"`
|
||||
Summary string `json:"summary"`
|
||||
Target string `json:"target"`
|
||||
IsTargetAdmin string `json:"isTargetAdmin"`
|
||||
}
|
||||
|
||||
type PerAccountData struct {
|
||||
FilePath string // Path to the JSON file
|
||||
PrivescPaths []CapeJSON // All records contained in the file
|
||||
}
|
||||
|
||||
type AllAccountData struct {
|
||||
Files map[string]*PerAccountData // Map of file paths to their records
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
s := table.DefaultStyles()
|
||||
s.Header = s.Header.
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
BorderBottom(true).
|
||||
Bold(false)
|
||||
s.Selected = s.Selected.
|
||||
Foreground(lipgloss.Color("229")).
|
||||
Background(lipgloss.Color("57")).
|
||||
Bold(false)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.terminalWidth = msg.Width
|
||||
m.terminalHeight = msg.Height - 7 // Adjust as needed
|
||||
|
||||
// Calculate heights based on the specified percentages
|
||||
awsAccountsHeight := int(float32(m.terminalHeight) * 0.2)
|
||||
halfHeight := int(float32(m.terminalHeight) * 0.4) // For the other two viewports
|
||||
|
||||
// Update dimensions for all viewports
|
||||
m.awsAccountsViewport.Width = m.terminalWidth - 4
|
||||
m.awsAccountsViewport.Height = awsAccountsHeight
|
||||
|
||||
m.mainTableViewport.Width = m.terminalWidth - 4
|
||||
m.mainTableViewport.Height = halfHeight
|
||||
m.awsAccountsTable.SetHeight(awsAccountsHeight - 2)
|
||||
m.awsAccountsTable.SetWidth(m.terminalWidth - 4)
|
||||
m.awsAccountsViewport.Width = m.terminalWidth - 4
|
||||
|
||||
m.detailsViewport.Width = m.terminalWidth - 4
|
||||
m.detailsViewport.Height = halfHeight
|
||||
m.mainTable.SetHeight(halfHeight - 2)
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Up):
|
||||
m.lastKey = "↑"
|
||||
case key.Matches(msg, m.keys.Down):
|
||||
m.lastKey = "↓"
|
||||
case key.Matches(msg, m.keys.Left):
|
||||
m.lastKey = "←"
|
||||
case key.Matches(msg, m.keys.Right):
|
||||
m.lastKey = "→"
|
||||
case key.Matches(msg, m.keys.Help):
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
case key.Matches(msg, m.keys.Tab):
|
||||
// Switch focus between the the three viewports by cycling focusSelector between 0, 1, and 2
|
||||
m.focusSelector = (m.focusSelector + 1) % 3
|
||||
//m.focusOnTable = !m.focusOnTable
|
||||
// add a case for shift and tab at the same time to to cycle the focusSelector backwards between 0, 1, and 2
|
||||
case key.Matches(msg, m.keys.ShiftTab):
|
||||
m.focusSelector = (m.focusSelector - 1) % 3
|
||||
|
||||
case key.Matches(msg, m.keys.Quit):
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
//var detailsData map[int]string
|
||||
//var mainTable table.Model
|
||||
var err error
|
||||
var awsCurrentRow int
|
||||
var mainCurrentRow int
|
||||
switch m.focusSelector {
|
||||
case 0:
|
||||
m.awsAccountsTable, cmd = m.awsAccountsTable.Update(msg)
|
||||
m.awsAccountsViewport.SetContent(m.awsAccountsTable.View())
|
||||
awsCurrentRow = m.awsAccountsTable.Cursor()
|
||||
if m.awsSelectedRow != awsCurrentRow {
|
||||
m.awsSelectedRow = awsCurrentRow
|
||||
// Update the viewport content based on the newly selected row
|
||||
// Load the file for the selected account and show the file content in the main table
|
||||
m.mainTable, m.detailsData, err = getRecordsForAccount(m.preloadedData.Files[m.awsAccountsTable.Rows()[awsCurrentRow][0]])
|
||||
if err != nil {
|
||||
// Handle error
|
||||
break
|
||||
}
|
||||
m.mainTable.SetStyles(s)
|
||||
m.mainTableViewport.SetContent(m.mainTable.View())
|
||||
m.detailsViewport.SetContent(m.detailsData[0])
|
||||
|
||||
}
|
||||
case 1:
|
||||
m.mainTable, cmd = m.mainTable.Update(msg)
|
||||
// Check if the selected row has changed
|
||||
m.mainTableViewport.SetContent(m.mainTable.View())
|
||||
|
||||
mainCurrentRow = m.mainTable.Cursor()
|
||||
if m.mainSelectedRow != mainCurrentRow {
|
||||
m.mainSelectedRow = mainCurrentRow
|
||||
// Update the viewport content based on the newly selected row
|
||||
if detail, ok := m.detailsData[m.mainSelectedRow]; ok {
|
||||
m.detailsViewport.SetContent(detail)
|
||||
} else {
|
||||
m.detailsViewport.SetContent(fmt.Sprintf("No details available for main row %d (aws row %d)", m.mainSelectedRow, m.awsSelectedRow))
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
m.detailsViewport, cmd = m.detailsViewport.Update(msg)
|
||||
}
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
// Define styles for active and inactive viewports
|
||||
activeBorderStyle := lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("#00FFFF")). // Cyan for active
|
||||
Padding(0)
|
||||
activeBorderStyle.PaddingLeft(1)
|
||||
activeBorderStyle.PaddingRight(1)
|
||||
|
||||
inactiveBorderStyle := lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("#808080")). // Gray for inactive
|
||||
Padding(0)
|
||||
inactiveBorderStyle.PaddingLeft(1)
|
||||
inactiveBorderStyle.PaddingRight(1)
|
||||
|
||||
// Conditionally apply border styles based on focus
|
||||
awsTableViewStyle := activeBorderStyle
|
||||
m.awsAccountsTable.SetStyles(m.defaultTableStyle)
|
||||
tableViewStyle := inactiveBorderStyle
|
||||
m.mainTable.SetStyles(m.defaultTableStyle)
|
||||
detailsViewStyle := inactiveBorderStyle
|
||||
|
||||
switch m.focusSelector {
|
||||
case 0:
|
||||
awsTableViewStyle = activeBorderStyle
|
||||
m.awsAccountsTable.SetStyles(m.defaultTableStyle)
|
||||
tableViewStyle = inactiveBorderStyle
|
||||
detailsViewStyle = inactiveBorderStyle
|
||||
case 1:
|
||||
awsTableViewStyle = inactiveBorderStyle
|
||||
tableViewStyle = activeBorderStyle
|
||||
m.mainTable.SetStyles(m.defaultTableStyle)
|
||||
detailsViewStyle = inactiveBorderStyle
|
||||
case 2:
|
||||
awsTableViewStyle = inactiveBorderStyle
|
||||
tableViewStyle = inactiveBorderStyle
|
||||
detailsViewStyle = activeBorderStyle
|
||||
}
|
||||
|
||||
// Render the table and details viewports with their styles (adjust based on focus)
|
||||
awsAccountsView := awsTableViewStyle.Render(m.awsAccountsViewport.View())
|
||||
tableView := tableViewStyle.Render(m.mainTableViewport.View())
|
||||
detailsView := detailsViewStyle.Render(m.detailsViewport.View())
|
||||
|
||||
// Combine all views
|
||||
fullView := lipgloss.JoinVertical(lipgloss.Top, awsAccountsView, tableView, detailsView)
|
||||
|
||||
helpView := m.help.View(m.keys)
|
||||
|
||||
return fmt.Sprintf("%s\n%s", fullView, helpView)
|
||||
}
|
||||
|
||||
func calculateMaxWidths(rows []table.Row) []int {
|
||||
|
||||
var maxWidths []int
|
||||
// make sure the length of the rows is greater than 0
|
||||
|
||||
if len(rows) > 0 {
|
||||
maxWidths = make([]int, len(rows[0]))
|
||||
} else {
|
||||
maxWidths = []int{30, 30, 30, 30}
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
for i, cell := range row {
|
||||
if len(cell) > maxWidths[i] {
|
||||
maxWidths[i] = len(cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxWidths
|
||||
|
||||
}
|
||||
|
||||
func (m model) footerView() string {
|
||||
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.detailsViewport.ScrollPercent()*100))
|
||||
line := strings.Repeat("─", max(0, m.detailsViewport.Width-lipgloss.Width(info)))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func loadFileRecords(filePath string) (*PerAccountData, error) {
|
||||
// Open the file
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Decode the JSON data
|
||||
var records []CapeJSON
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&records); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return a new FileRecords instance
|
||||
return &PerAccountData{
|
||||
FilePath: filePath,
|
||||
PrivescPaths: records,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func preloadData(filePaths []string) (*AllAccountData, error) {
|
||||
appData := &AllAccountData{
|
||||
Files: make(map[string]*PerAccountData),
|
||||
}
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
fileRecords, err := loadFileRecords(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appData.Files[filePath] = fileRecords
|
||||
}
|
||||
|
||||
return appData, nil
|
||||
}
|
||||
|
||||
func getRecordsForAccount(preloadedData *PerAccountData) (table.Model, map[int]string, error) {
|
||||
// lets load the records for the first file in the list
|
||||
|
||||
records := preloadedData.PrivescPaths
|
||||
if len(records) < 1 {
|
||||
mainTable := table.New(
|
||||
table.WithColumns([]table.Column{
|
||||
{Title: "Account", Width: 30},
|
||||
{Title: "Source", Width: 30},
|
||||
{Title: "Target", Width: 30},
|
||||
{Title: "isTargetAdmin", Width: 30},
|
||||
}),
|
||||
table.WithRows([]table.Row{{"No records found", "", "", ""}}),
|
||||
table.WithFocused(true),
|
||||
)
|
||||
return mainTable, nil, nil
|
||||
|
||||
}
|
||||
|
||||
// Prepare rows for the table and data for the right view
|
||||
rows := make([]table.Row, 0, len(records)-1)
|
||||
detailsData := make(map[int]string) // Initialize the map for the fourth column's data
|
||||
for i, record := range records[1:] { // Skip the header row
|
||||
rows = append(rows, table.Row{record.Account, record.Source, record.Target, record.IsTargetAdmin})
|
||||
detailsData[i] = expandDetailsData(record.Summary)
|
||||
}
|
||||
|
||||
// Calculate max widths
|
||||
maxWidths := calculateMaxWidths(rows)
|
||||
|
||||
// Define columns with calculated widths
|
||||
columns := make([]table.Column, len(maxWidths))
|
||||
colNames := []string{}
|
||||
for _, header := range []string{"Account", "Source", "Target", "isTargetAdmin"} {
|
||||
colNames = append(colNames, header)
|
||||
}
|
||||
|
||||
for i, width := range maxWidths {
|
||||
colName := colNames[i]
|
||||
// if column name width is greater than the calculated width, use the column name width
|
||||
if len(colName) > width {
|
||||
width = len(colName)
|
||||
}
|
||||
columns[i] = table.Column{
|
||||
Title: colName,
|
||||
Width: width,
|
||||
}
|
||||
}
|
||||
|
||||
mainTable := table.New(
|
||||
table.WithColumns(columns),
|
||||
table.WithRows(rows),
|
||||
table.WithFocused(true),
|
||||
)
|
||||
return mainTable, detailsData, nil
|
||||
}
|
||||
|
||||
func CapeTUI(outputFiles []string) {
|
||||
|
||||
preloadData, err := preloadData(outputFiles)
|
||||
if err != nil {
|
||||
fmt.Printf("Error preloading data: %s\n", err)
|
||||
fmt.Println("Either remove this profile from the list of profiles, or make sure cape can run successfully for this profile")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var awsAccountsRows []table.Row
|
||||
for _, file := range preloadData.Files {
|
||||
// Extract a user-friendly account name from the file path if needed
|
||||
// For simplicity, here we use the file path itself
|
||||
awsAccountsRows = append(awsAccountsRows, table.Row{file.FilePath})
|
||||
}
|
||||
|
||||
mainTable, detailsData, err := getRecordsForAccount(preloadData.Files[outputFiles[0]])
|
||||
|
||||
s := table.DefaultStyles()
|
||||
s.Header = s.Header.
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
BorderBottom(true).
|
||||
Bold(false)
|
||||
s.Selected = s.Selected.
|
||||
Foreground(lipgloss.Color("229")).
|
||||
Background(lipgloss.Color("57")).
|
||||
Bold(false)
|
||||
|
||||
awsAccountsWidth := calculateMaxWidths(awsAccountsRows)
|
||||
awsAccountsTable := table.New(
|
||||
table.WithColumns([]table.Column{
|
||||
{Title: "AWS Accounts", Width: awsAccountsWidth[0]},
|
||||
}),
|
||||
table.WithRows(awsAccountsRows),
|
||||
table.WithFocused(true), // Initially unfocused
|
||||
)
|
||||
|
||||
awsAccountsTable.SetStyles(s)
|
||||
awsAccountsViewport := viewport.New(0, 0) // Initialize with 0 size; will be updated
|
||||
awsAccountsViewport.SetContent(awsAccountsTable.View()) // Set initial content
|
||||
|
||||
mainTable.SetStyles(s)
|
||||
mainTableViewport := viewport.New(0, 0) // Initialize with 0 size; it will be updated
|
||||
mainTableViewport.SetContent(mainTable.View()) // Set the initial content of the table viewport
|
||||
|
||||
// Initialize viewport for details view
|
||||
detailsViewportModel := viewport.New(0, 0) // Size will be set based on terminal size in Update
|
||||
// show the first row's details by default
|
||||
detailsViewportModel.SetContent(detailsData[0])
|
||||
|
||||
m := model{
|
||||
preloadedData: preloadData,
|
||||
awsAccountsTable: awsAccountsTable,
|
||||
awsAccountsViewport: awsAccountsViewport,
|
||||
mainTable: mainTable,
|
||||
mainTableViewport: mainTableViewport,
|
||||
detailsData: detailsData,
|
||||
detailsViewport: detailsViewportModel,
|
||||
|
||||
//focusOnTable: true,
|
||||
focusSelector: 0,
|
||||
mainSelectedRow: 0, // Initialize selectedRow with an invalid index
|
||||
awsSelectedRow: 0,
|
||||
defaultTableStyle: s,
|
||||
keys: keys,
|
||||
help: help.New(),
|
||||
}
|
||||
|
||||
p := tea.NewProgram(m)
|
||||
if err := p.Start(); err != nil {
|
||||
fmt.Printf("Error starting program: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// keyMap defines a set of keybindings. To work for help it must satisfy
|
||||
// key.Map. It could also very easily be a map[string]key.Binding.
|
||||
type keyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
Tab key.Binding
|
||||
ShiftTab key.Binding
|
||||
}
|
||||
|
||||
// ShortHelp returns keybindings to be shown in the mini help view. It's part
|
||||
// of the key.Map interface.
|
||||
func (k keyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Tab, k.ShiftTab, k.Up, k.Down, k.Help, k.Quit}
|
||||
}
|
||||
|
||||
// FullHelp returns keybindings for the expanded help view. It's part of the
|
||||
// key.Map interface.
|
||||
func (k keyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Left, k.Right}, // first column
|
||||
{k.Tab, k.ShiftTab, k.Help, k.Quit}, // second column
|
||||
}
|
||||
}
|
||||
|
||||
var keys = keyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑/k", "move up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓/j", "move down"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("←/h", "move left"),
|
||||
),
|
||||
Right: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("→/l", "move right"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "toggle help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q", "esc", "ctrl+c"),
|
||||
key.WithHelp("q", "quit"),
|
||||
),
|
||||
Tab: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("tab", "Switch window focus"),
|
||||
),
|
||||
ShiftTab: key.NewBinding(
|
||||
key.WithKeys("shift+tab"),
|
||||
key.WithHelp("shift+tab", "Switch window focus"),
|
||||
),
|
||||
}
|
||||
|
||||
func expandDetailsData(input string) string {
|
||||
output := ""
|
||||
lines := strings.Split(input, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
var hop, option int
|
||||
var rest string
|
||||
|
||||
// Use Sscanf to extract hop and option values
|
||||
n, err := fmt.Sscanf(line, "[Hop: %d] [Option: %d]", &hop, &option)
|
||||
if err != nil || n != 2 {
|
||||
fmt.Printf("Error parsing line, expected 2 got %d: %v\n", n, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the rest of the line manually
|
||||
restIndex := strings.Index(line, "]") + 1
|
||||
restIndex = strings.Index(line[restIndex:], "]") + restIndex + 1
|
||||
if restIndex > 1 && restIndex < len(line) {
|
||||
rest = line[restIndex+1:] // +1 to skip the space after the second ]
|
||||
}
|
||||
|
||||
// Construct the formatted output
|
||||
formattedLine := fmt.Sprintf("[Hop: %d][Option: %d]\n\t%s\n\n", hop, option, rest)
|
||||
output += formattedLine
|
||||
}
|
||||
|
||||
return output
|
||||
|
||||
}
|
||||
+1049
File diff suppressed because it is too large.
Load diff
@@ -3,11 +3,13 @@ package aws
|
||||
import (
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/fsx"
|
||||
"github.com/aws/aws-sdk-go-v2/service/glue"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
"github.com/aws/aws-sdk-go-v2/service/organizations"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
@@ -15,15 +17,17 @@ 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 initIAMSimClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
|
||||
func InitIamCommandClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
|
||||
|
||||
iamSimMod := IamSimulatorModule{
|
||||
IAMClient: iamSimPPClient,
|
||||
Caller: caller,
|
||||
AWSProfileProvided: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
modLog: internal.TxtLog.WithFields(logrus.Fields{"module": "iam-simulator"}),
|
||||
}
|
||||
|
||||
return iamSimMod
|
||||
@@ -116,7 +120,7 @@ func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string
|
||||
return fileSystemsClient
|
||||
}
|
||||
|
||||
func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) OrgModule {
|
||||
func InitOrgsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) OrgModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
orgClient := OrgModule{
|
||||
OrganizationsClient: organizations.NewFromConfig(AWSConfig),
|
||||
@@ -127,6 +131,17 @@ func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVers
|
||||
return orgClient
|
||||
}
|
||||
|
||||
func InitPermissionsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) IamPermissionsModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
permissionsClient := IamPermissionsModule{
|
||||
IAMClient: iam.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
}
|
||||
return permissionsClient
|
||||
}
|
||||
|
||||
func InitSecretsManagerClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) *secretsmanager.Client {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
return secretsmanager.NewFromConfig(AWSConfig)
|
||||
@@ -136,3 +151,11 @@ func InitGlueClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVer
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
return glue.NewFromConfig(AWSConfig)
|
||||
}
|
||||
|
||||
func InitOrgClient(AWSConfig aws.Config) *organizations.Client {
|
||||
return organizations.NewFromConfig(AWSConfig)
|
||||
}
|
||||
|
||||
func InitIAMClient(AWSConfig aws.Config) *iam.Client {
|
||||
return iam.NewFromConfig(AWSConfig)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+14
-8
@@ -19,15 +19,17 @@ type CodeBuildModule struct {
|
||||
CodeBuildClient sdk.CodeBuildClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
PmapperDataBasePath string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
@@ -64,8 +66,8 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating CodeBuild projects 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.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
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)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
@@ -210,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 {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
dsTypes "github.com/aws/aws-sdk-go-v2/service/directoryservice/types"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type DirectoryModule struct {
|
||||
// General configuration data
|
||||
DSClient sdk.AWSDSClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSProfile string
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
AWSMFAToken string
|
||||
AWSConfig aws.Config
|
||||
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
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Directory struct {
|
||||
DirectoryId string
|
||||
DNS string
|
||||
NetBios string
|
||||
AccessURL string
|
||||
Alias string
|
||||
OsVersion string
|
||||
Region string
|
||||
TrustInfo string
|
||||
}
|
||||
|
||||
func (m *DirectoryModule) PrintDirectories(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "directory-services"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
|
||||
if m.AWSProfileProvided == "" {
|
||||
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
|
||||
} else {
|
||||
m.AWSProfileStub = m.AWSProfileProvided
|
||||
}
|
||||
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 Cloud Directories with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
|
||||
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
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Directory)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Name",
|
||||
"Alias",
|
||||
"Domain",
|
||||
"NetBIOS name",
|
||||
"Access URL",
|
||||
"Version",
|
||||
"Trusts",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Name",
|
||||
"Alias",
|
||||
"Domain",
|
||||
"NetBIOS name",
|
||||
"Access URL",
|
||||
"Version",
|
||||
"Trusts",
|
||||
}
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Name",
|
||||
"Domain",
|
||||
"NetBIOS name",
|
||||
"Access URL",
|
||||
"Version",
|
||||
"Trusts",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Table rows
|
||||
for i := range m.Directories {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Directories[i].DirectoryId,
|
||||
m.Directories[i].Alias,
|
||||
m.Directories[i].DNS,
|
||||
m.Directories[i].NetBios,
|
||||
m.Directories[i].AccessURL,
|
||||
m.Directories[i].OsVersion,
|
||||
m.Directories[i].TrustInfo,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfileStub
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s directories found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), strconv.Itoa(len(m.output.Body)))
|
||||
//fmt.Printf("[%s][%s] Resource policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No directories 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 *DirectoryModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
|
||||
defer wg.Done()
|
||||
|
||||
// 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("directoryservice", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getDirectoriesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DirectoryModule) Receiver(receiver chan Directory, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Directories = append(m.Directories, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
func (m *DirectoryModule) getDirectoriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
|
||||
// Get directories
|
||||
directories, err := sdk.CachedDSDescribeDirectories(m.DSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
for _, directory := range directories {
|
||||
trusts, err := sdk.CachedDSDescribeTrusts(m.DSClient, aws.ToString(m.Caller.Account), r, *directory.DirectoryId)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
dataReceiver <- Directory{
|
||||
DirectoryId: *directory.DirectoryId,
|
||||
DNS: *directory.Name,
|
||||
NetBios: *directory.ShortName,
|
||||
Region: r,
|
||||
AccessURL: *directory.AccessUrl,
|
||||
Alias: *directory.Alias,
|
||||
OsVersion: fmt.Sprintf("%s", directory.OsVersion),
|
||||
TrustInfo: m.formatTrusts(trusts),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DirectoryModule) formatTrusts(t []dsTypes.Trust) string {
|
||||
var output string = ""
|
||||
for idx, trust := range t {
|
||||
if idx != 0 {
|
||||
output = output + "\n"
|
||||
}
|
||||
if trust.TrustDirection == "One-Way: Outgoing" {
|
||||
output = output + "→"
|
||||
} else if trust.TrustDirection == "One-Way: Ingoing" {
|
||||
output = output + "←"
|
||||
} else {
|
||||
output = output + "↔"
|
||||
}
|
||||
output = fmt.Sprintf("%s %s", output, *trust.RemoteDomainName)
|
||||
// check trust type (external or forest)
|
||||
if trust.TrustType == "External" {
|
||||
output = fmt.Sprintf("%s (%s)", output, "Domain")
|
||||
} else {
|
||||
output = fmt.Sprintf("%s (%s)", output, "Forest")
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
+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 {
|
||||
|
||||
+32
-10
@@ -25,15 +25,17 @@ type ECSTasksModule struct {
|
||||
EC2Client sdk.AWSEC2ClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
PmapperDataBasePath string
|
||||
|
||||
AWSProfile string
|
||||
Goroutines int
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
@@ -49,6 +51,7 @@ type MappedECSTask struct {
|
||||
Cluster string
|
||||
TaskDefinitionName string
|
||||
TaskDefinitionContent string
|
||||
ContainerName string
|
||||
LaunchType string
|
||||
ID string
|
||||
ExternalIP string
|
||||
@@ -73,8 +76,8 @@ func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) {
|
||||
fmt.Printf("[%s][%s] Enumerating ECS tasks in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
|
||||
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -144,6 +147,7 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
|
||||
"Account",
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"ContainerName",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
@@ -170,6 +174,7 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
|
||||
"Account",
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"ContainerName",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
@@ -183,6 +188,7 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
|
||||
tableCols = []string{
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"ContainerName",
|
||||
"LaunchType",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
@@ -205,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,
|
||||
@@ -301,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 {
|
||||
@@ -367,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 != "" {
|
||||
@@ -393,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),
|
||||
@@ -504,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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+14
-8
@@ -22,15 +22,17 @@ type EKSModule struct {
|
||||
EKSClient sdk.EKSClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
PmapperDataBasePath string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
@@ -72,8 +74,8 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
|
||||
fmt.Printf("[%s][%s] Enumerating EKS clusters for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
|
||||
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -241,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 {
|
||||
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
ingestor "github.com/BishopFox/cloudfox/aws/graph/ingester"
|
||||
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"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/sts"
|
||||
"github.com/bishopfox/knownawsaccountslookup"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type GraphCommand struct {
|
||||
|
||||
// General configuration data
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
Verbosity int
|
||||
AWSOutputDirectory string
|
||||
AWSConfig aws.Config
|
||||
Version string
|
||||
SkipAdminCheck bool
|
||||
PmapperDataBasePath string
|
||||
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
|
||||
vendors *knownawsaccountslookup.Vendors
|
||||
|
||||
// Main module data
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
func (m *GraphCommand) RunGraphCommand() {
|
||||
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = m.Verbosity
|
||||
m.output.Directory = m.AWSOutputDirectory
|
||||
m.output.CallingModule = "graph"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
m.vendors = knownawsaccountslookup.NewVendorMap()
|
||||
m.vendors.PopulateKnownAWSAccounts()
|
||||
|
||||
m.modLog.Info("Collecting data for graph ingestor...")
|
||||
|
||||
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
|
||||
|
||||
////////////////
|
||||
// Accounts
|
||||
////////////////
|
||||
|
||||
accounts := m.collectAccountDataForGraph()
|
||||
// write data to jsonl file for ingestor to read
|
||||
fileName := fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "accounts")
|
||||
// create file and directory if it doesn't exist
|
||||
if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
|
||||
m.modLog.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
outputFile, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
return
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
for _, account := range accounts {
|
||||
if err := internal.WriteJsonlFile(outputFile, account); err != nil {
|
||||
m.modLog.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
////////////////
|
||||
// Users
|
||||
////////////////
|
||||
|
||||
// users := m.collectUserDataForGraph()
|
||||
// // write data to jsonl file for ingestor to read
|
||||
// fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "users")
|
||||
// // create file and directory if it doesn't exist
|
||||
// if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
|
||||
// m.modLog.Error(err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
// if err != nil {
|
||||
// m.modLog.Error(err)
|
||||
// return
|
||||
// }
|
||||
// defer outputFile.Close()
|
||||
|
||||
// for _, user := range users {
|
||||
// if err := internal.WriteJsonlFile(outputFile, user); err != nil {
|
||||
// m.modLog.Error(err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
////////////////
|
||||
// Roles
|
||||
////////////////
|
||||
|
||||
roles := m.collectRoleDataForGraph()
|
||||
// write data to jsonl file for ingestor to read
|
||||
fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "roles")
|
||||
// create file and directory if it doesn't exist
|
||||
if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
|
||||
m.modLog.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
return
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
for _, role := range roles {
|
||||
if err := internal.WriteJsonlFile(outputFile, role); err != nil {
|
||||
m.modLog.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ingestor, err := ingestor.NewCloudFoxIngestor()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Pmapper hack
|
||||
|
||||
goCtx := context.Background()
|
||||
sharedLogger.Infof("Verifying connectivity to Neo4J at %s", ingestor.Uri)
|
||||
if err := ingestor.Driver.VerifyConnectivity(goCtx); err != nil {
|
||||
sharedLogger.Error(err)
|
||||
}
|
||||
defer ingestor.Driver.Close(goCtx)
|
||||
m.pmapperMod.GenerateCypherStatements(goCtx, ingestor.Driver)
|
||||
|
||||
// back to regular stuff
|
||||
|
||||
ingestor.Run(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)))
|
||||
|
||||
}
|
||||
|
||||
func (m *GraphCommand) collectAccountDataForGraph() []models.Account {
|
||||
//OrganizationsCommandClient := InitOrgClient(m.AWSConfig)
|
||||
var accounts []models.Account
|
||||
OrganizationsCommandClient := InitOrgClient(m.AWSConfig)
|
||||
DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(OrganizationsCommandClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Fatal(err)
|
||||
}
|
||||
if DescribeOrgOutput.MasterAccountId == nil {
|
||||
m.modLog.Error("Organization is not configured")
|
||||
}
|
||||
// If the account is not the org mgmt account, it can only see some info about itself and some info about the org mgmt account.
|
||||
// populate both of them here.
|
||||
if aws.ToString(DescribeOrgOutput.MasterAccountId) != aws.ToString(m.Caller.Account) {
|
||||
|
||||
//create new object of type models.Account for this account
|
||||
thisAccount := models.Account{
|
||||
Id: aws.ToString(m.Caller.Account),
|
||||
Arn: fmt.Sprintf("arn:aws:iam::%s:root", aws.ToString(m.Caller.Account)),
|
||||
//Name: aws.ToString(m.Caller.Account),
|
||||
IsOrgMgmt: false,
|
||||
IsChildAccount: true,
|
||||
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
|
||||
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
|
||||
}
|
||||
accounts = append(accounts, thisAccount)
|
||||
|
||||
//create new object of type models.Account for the mgmt account
|
||||
mgmtAccount := models.Account{
|
||||
Id: aws.ToString(DescribeOrgOutput.MasterAccountId),
|
||||
Arn: aws.ToString(DescribeOrgOutput.MasterAccountArn),
|
||||
Email: aws.ToString(DescribeOrgOutput.MasterAccountEmail),
|
||||
//Status: string(account.Status),
|
||||
//JoinedMethod: string(account.JoinedMethod),
|
||||
//JoinedTimestamp: account.JoinedTimestamp.String(),
|
||||
IsOrgMgmt: true,
|
||||
IsChildAccount: false,
|
||||
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
|
||||
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
|
||||
}
|
||||
accounts = append(accounts, mgmtAccount)
|
||||
return accounts
|
||||
|
||||
} else {
|
||||
// In this case we are the org mgmt account, so we can see all the accounts in the org.
|
||||
ListAccounts, err := sdk.CachedOrganizationsListAccounts(OrganizationsCommandClient, aws.ToString(DescribeOrgOutput.MasterAccountId))
|
||||
if err != nil {
|
||||
m.modLog.Fatal(err)
|
||||
}
|
||||
for _, account := range ListAccounts {
|
||||
var isMgmtAccount bool
|
||||
var isChildAccount bool
|
||||
m.modLog.Info("Account: ", aws.ToString(account.Name))
|
||||
if aws.ToString(DescribeOrgOutput.MasterAccountId) == aws.ToString(account.Id) {
|
||||
// this is the org mgmt account
|
||||
isMgmtAccount = true
|
||||
isChildAccount = false
|
||||
} else if DescribeOrgOutput.MasterAccountId == nil {
|
||||
// this is a standalone account
|
||||
isChildAccount = false
|
||||
isMgmtAccount = false
|
||||
} else {
|
||||
// this is a child account
|
||||
isChildAccount = true
|
||||
isMgmtAccount = false
|
||||
}
|
||||
|
||||
//create new object of type models.Account
|
||||
account := models.Account{
|
||||
Id: aws.ToString(account.Id),
|
||||
Arn: aws.ToString(account.Arn),
|
||||
Name: aws.ToString(account.Name),
|
||||
Email: aws.ToString(account.Email),
|
||||
Status: string(account.Status),
|
||||
JoinedMethod: string(account.JoinedMethod),
|
||||
JoinedTimestamp: account.JoinedTimestamp.String(),
|
||||
IsOrgMgmt: isMgmtAccount,
|
||||
IsChildAccount: isChildAccount,
|
||||
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
|
||||
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
|
||||
}
|
||||
accounts = append(accounts, account)
|
||||
}
|
||||
return accounts
|
||||
}
|
||||
}
|
||||
|
||||
func (m *GraphCommand) collectRoleDataForGraph() []models.Role {
|
||||
var isAdmin, canPrivEscToAdmin string
|
||||
|
||||
// iamClient := InitIAMClient(m.AWSConfig)
|
||||
// iamSimClient := InitIamCommandClient(iamClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
// localAdminMap := make(map[string]bool)
|
||||
|
||||
var roles []models.Role
|
||||
IAMCommandClient := InitIAMClient(m.AWSConfig)
|
||||
ListRolesOutput, err := sdk.CachedIamListRoles(IAMCommandClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
|
||||
for _, role := range ListRolesOutput {
|
||||
accountId := strings.Split(aws.ToString(role.Arn), ":")[4]
|
||||
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
// if m.pmapperError == nil {
|
||||
// isAdmin, canPrivEscToAdmin = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, role.Arn)
|
||||
// } else {
|
||||
// isAdmin, canPrivEscToAdmin = GetIamSimResult(m.SkipAdminCheck, role.Arn, iamSimClient, localAdminMap)
|
||||
// }
|
||||
|
||||
// for _, row := range m.PermissionRowsFromAllProfiles {
|
||||
// if row.Arn == aws.ToString(role.Arn) {
|
||||
|
||||
// // look for cases where there is a permission that allows sts:assumeRole or *
|
||||
// // lowercase the action and compare it against the list of checks below
|
||||
// if strings.EqualFold(row.Action, "sts:AssumeRole") ||
|
||||
// strings.EqualFold(row.Action, "*") ||
|
||||
// strings.EqualFold(row.Action, "sts:Assume*") ||
|
||||
// strings.EqualFold(row.Action, "sts:*") {
|
||||
|
||||
// if row.Effect == "Allow" {
|
||||
// //PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
|
||||
// if row.Resource == "*" {
|
||||
// PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
|
||||
// } else if strings.EqualFold(row.Resource, aws.ToString(role.Arn)) {
|
||||
// PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if row.Effect == "Deny" {
|
||||
// // Remove the string sts:AssumeRole from the PrivEscPermissions slice
|
||||
// for i, v := range PrivEscPermissions {
|
||||
// if v == "sts:AssumeRole" {
|
||||
// PrivEscPermissions = append(PrivEscPermissions[:i], PrivEscPermissions[i+1:]...)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
var TrustedPrincipals []models.TrustedPrincipal
|
||||
var TrustedServices []models.TrustedService
|
||||
var TrustedFederatedProviders []models.TrustedFederatedProvider
|
||||
//var TrustedFederatedSubjects string
|
||||
var trustedProvider string
|
||||
var trustedSubjects string
|
||||
var vendorName string
|
||||
|
||||
for _, statement := range trustsdoc.Statement {
|
||||
for _, principal := range statement.Principal.AWS {
|
||||
if strings.Contains(principal, ":root") {
|
||||
//check to see if the accountID is known
|
||||
accountID := strings.Split(principal, ":")[4]
|
||||
vendorName = m.vendors.GetVendorNameFromAccountID(accountID)
|
||||
}
|
||||
|
||||
TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{
|
||||
TrustedPrincipal: principal,
|
||||
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
|
||||
VendorName: vendorName,
|
||||
//IsAdmin: false,
|
||||
//CanPrivEscToAdmin: false,
|
||||
})
|
||||
|
||||
}
|
||||
for _, service := range statement.Principal.Service {
|
||||
TrustedServices = append(TrustedServices, models.TrustedService{
|
||||
TrustedService: service,
|
||||
AccountID: accountId,
|
||||
//IsAdmin: false,
|
||||
//CanPrivEscToAdmin: false,
|
||||
})
|
||||
|
||||
}
|
||||
for _, federated := range statement.Principal.Federated {
|
||||
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
|
||||
trustedProvider = "GitHub"
|
||||
trustedSubjects := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",")
|
||||
if trustedSubjects == "" {
|
||||
trustedSubjects = "ALL REPOS!!!"
|
||||
} else {
|
||||
trustedSubjects = "Repos: " + trustedSubjects
|
||||
}
|
||||
|
||||
} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
|
||||
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
|
||||
trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
|
||||
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
|
||||
trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")"
|
||||
}
|
||||
trustedSubjects = "Not applicable"
|
||||
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != nil || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != nil {
|
||||
trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")"
|
||||
// if statement.Condition.StringEquals.OidcEksSub != "" {
|
||||
// trustedSubjects = statement.Condition.StringEquals.OidcEksSub
|
||||
// } else if statement.Condition.StringLike.OidcEksSub != "" {
|
||||
// trustedSubjects = statement.Condition.StringLike.OidcEksSub
|
||||
// } else {
|
||||
// trustedSubjects = "ALL SERVICE ACCOUNTS!"
|
||||
// }
|
||||
trustedSubjects = "ALL SERVICE ACCOUNTS!"
|
||||
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
|
||||
trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")"
|
||||
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
|
||||
trustedSubjects = statement.Condition.ForAnyValueStringLike.CognitoAMR
|
||||
}
|
||||
} else {
|
||||
if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") {
|
||||
trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")"
|
||||
trustedSubjects = "ALL SERVICE ACCOUNTS!"
|
||||
} else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
|
||||
trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
|
||||
}
|
||||
trustedSubjects = "Not applicable"
|
||||
}
|
||||
|
||||
TrustedFederatedProviders = append(TrustedFederatedProviders, models.TrustedFederatedProvider{
|
||||
TrustedFederatedProvider: federated,
|
||||
ProviderShortName: trustedProvider,
|
||||
TrustedSubjects: trustedSubjects,
|
||||
//IsAdmin: false,
|
||||
//CanPrivEscToAdmin: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//create new object of type models.Role
|
||||
role := models.Role{
|
||||
Id: aws.ToString(role.Arn),
|
||||
AccountID: accountId,
|
||||
ARN: aws.ToString(role.Arn),
|
||||
Name: aws.ToString(role.RoleName),
|
||||
TrustsDoc: trustsdoc,
|
||||
TrustedPrincipals: TrustedPrincipals,
|
||||
TrustedServices: TrustedServices,
|
||||
TrustedFederatedProviders: TrustedFederatedProviders,
|
||||
CanPrivEscToAdmin: canPrivEscToAdmin,
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
roles = append(roles, role)
|
||||
}
|
||||
return roles
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package collectors
|
||||
@@ -0,0 +1,283 @@
|
||||
package ingestor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
|
||||
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models"
|
||||
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// Neo4j
|
||||
MergeNodeQueryTemplate = `CALL apoc.merge.node([$labels[0]], {Id: $Id}, $properties, $properties) YIELD node as obj
|
||||
CALL apoc.create.setLabels(obj, $labels) YIELD node as labeledObj
|
||||
RETURN labeledObj`
|
||||
|
||||
MergeRelationQueryTemplate = `UNWIND $batch as row
|
||||
CALL apoc.merge.node([row.sourceLabel], apoc.map.fromValues([row.sourceProperty, row.sourceNodeId])) YIELD node as from
|
||||
CALL apoc.merge.node([row.targetLabel], apoc.map.fromValues([row.targetProperty, row.targetNodeId])) YIELD node as to
|
||||
CALL apoc.merge.relationship(from, row.relationshipType, {}, row.properties, to) YIELD rel
|
||||
RETURN rel`
|
||||
|
||||
// Using sprintf to insert the label name since the driver doesn't support parameters for labels here
|
||||
// %[1]s is a nice way to say "insert the first parameter here"
|
||||
CreateConstraintQueryTemplate = "CREATE CONSTRAINT IF NOT EXISTS FOR (n: %s) REQUIRE n.Id IS UNIQUE"
|
||||
CreateIndexQueryTemplate = "CREATE INDEX %[1]s_Id IF NOT EXISTS FOR (n: %[1]s) ON (n.Id)"
|
||||
|
||||
PostProcessMergeQueryTemplate = `MATCH (n)
|
||||
WITH n.Id AS Id, COLLECT(n) AS nodesToMerge
|
||||
WHERE size(nodesToMerge) > 1
|
||||
CALL apoc.refactor.mergeNodes(nodesToMerge, {properties: 'combine', mergeRels:true})
|
||||
YIELD node
|
||||
RETURN count(*);`
|
||||
)
|
||||
|
||||
type Neo4jConfig struct {
|
||||
Uri string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type CloudFoxIngestor struct {
|
||||
Neo4jConfig
|
||||
//ResultsFile string
|
||||
Driver neo4j.DriverWithContext
|
||||
TmpDir string
|
||||
}
|
||||
|
||||
func NewCloudFoxIngestor() (*CloudFoxIngestor, error) {
|
||||
config := Neo4jConfig{
|
||||
Uri: "neo4j://localhost:7687",
|
||||
Username: "neo4j",
|
||||
Password: "cloudfox",
|
||||
}
|
||||
driver, err := neo4j.NewDriverWithContext(config.Uri, neo4j.BasicAuth(config.Username, config.Password, ""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CloudFoxIngestor{
|
||||
Neo4jConfig: config,
|
||||
//ResultsFile: resultsFile,
|
||||
Driver: driver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// func unzipToTemp(zipFilePath string) (string, error) {
|
||||
// // Create a temporary directory to extract the zip file to
|
||||
// tempDir, err := os.MkdirTemp("", "cloudfox-graph")
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
|
||||
// // Open the zip file and extract to a temporary directory
|
||||
// zipfile, err := zip.OpenReader(zipFilePath)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// defer zipfile.Close()
|
||||
|
||||
// for _, file := range zipfile.File {
|
||||
// path := filepath.Join(tempDir, file.Name)
|
||||
// log.Debugf("Extracting file: %s", path)
|
||||
|
||||
// fileData, err := file.Open()
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// defer fileData.Close()
|
||||
|
||||
// newFile, err := os.Create(path)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// defer newFile.Close()
|
||||
|
||||
// if _, err := io.Copy(newFile, fileData); err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// }
|
||||
// return tempDir, nil
|
||||
// }
|
||||
|
||||
func (i *CloudFoxIngestor) ProcessFile(path string, info os.FileInfo) error {
|
||||
log.Infof("Processing file: %s", info.Name())
|
||||
|
||||
switch info.Name() {
|
||||
case "accounts.jsonl":
|
||||
return i.ProcessFileObjects(path, schema.Account, schema.Account)
|
||||
case "roles.jsonl":
|
||||
return i.ProcessFileObjects(path, schema.Role, schema.Role)
|
||||
// case "servicePrincipals.jsonl":
|
||||
// return i.ProcessFileObjects(path, schema.GraphServicePrincipal, schema.GraphObject)
|
||||
// case "applications.jsonl":
|
||||
// return i.ProcessFileObjects(path, schema.GraphApplication, schema.GraphObject)
|
||||
// case "devices.jsonl":
|
||||
// return i.ProcessFileObjects(path, schema.GraphDevice, schema.GraphObject)
|
||||
// case "directoryRoles.jsonl":
|
||||
// return i.ProcessFileObjects(path, schema.GraphRole, schema.GraphObject)
|
||||
// case "subscriptions.jsonl":
|
||||
// return i.ProcessFileObjects(path, schema.Subscription, schema.ArmResource)
|
||||
// case "tenants.jsonl":
|
||||
// return i.ProcessFileObjects(path, schema.Tenant, schema.ArmResource)
|
||||
// case "rbac.jsonl":
|
||||
// return i.ProcessFileObjects(path, schema.AzureRbac, "")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *CloudFoxIngestor) ProcessFileObjects(path string, objectType schema.NodeLabel, generalType schema.NodeLabel) error {
|
||||
|
||||
var object = models.NodeLabelToNodeMap[objectType]
|
||||
|
||||
// Open the file
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the file line by line
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
//Iterate over the lines and create the nodes
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSuffix(scanner.Text(), "\n")
|
||||
|
||||
// Skip empty lines
|
||||
if len(line) > 0 {
|
||||
if err := json.Unmarshal([]byte(line), &object); err != nil {
|
||||
log.Errorf("%s : %s", err, line)
|
||||
continue
|
||||
}
|
||||
}
|
||||
relationships := object.MakeRelationships()
|
||||
if err := i.InsertDBObjects(object, relationships, []schema.NodeLabel{generalType, objectType}); err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *CloudFoxIngestor) InsertDBObjects(object schema.Node, relationships []schema.Relationship, labels []schema.NodeLabel) error {
|
||||
goCtx := context.Background()
|
||||
var err error
|
||||
|
||||
// Insert the node
|
||||
if object != nil {
|
||||
nodeMap, err := schema.ConvertCustomTypesToNeo4j(&object)
|
||||
if err != nil {
|
||||
log.Errorf("Error converting custom types to neo4j: %s -- %v", err, object)
|
||||
return err
|
||||
}
|
||||
|
||||
//nodeMap := schema.AsNeo4j(&object)
|
||||
nodeQueryParams := map[string]interface{}{
|
||||
"Id": nodeMap["Id"],
|
||||
"labels": labels,
|
||||
"properties": nodeMap,
|
||||
}
|
||||
_, err = neo4j.ExecuteQuery(goCtx, i.Driver, MergeNodeQueryTemplate, nodeQueryParams, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
|
||||
if err != nil {
|
||||
log.Errorf("Error inserting node: %s -- %v", err, nodeQueryParams)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the relationships
|
||||
if len(relationships) > 0 {
|
||||
var relationshipInterface []map[string]interface{}
|
||||
|
||||
// Check the default SourceProperty and TargetProperty values
|
||||
for _, relationship := range relationships {
|
||||
var currentRelationship map[string]interface{}
|
||||
|
||||
if relationship.SourceProperty == "" {
|
||||
relationship.SourceProperty = "Id"
|
||||
}
|
||||
if relationship.TargetProperty == "" {
|
||||
relationship.TargetProperty = "Id"
|
||||
}
|
||||
relationshipBytes, err := json.Marshal(relationship)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(relationshipBytes, ¤tRelationship); err != nil {
|
||||
return err
|
||||
}
|
||||
relationshipInterface = append(relationshipInterface, currentRelationship)
|
||||
}
|
||||
|
||||
_, err = neo4j.ExecuteQuery(goCtx, i.Driver, MergeRelationQueryTemplate, map[string]interface{}{"batch": relationshipInterface}, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
|
||||
if err != nil {
|
||||
log.Errorf("Error inserting relationships: %s -- %v", err, relationshipInterface)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *CloudFoxIngestor) Run(graphOutputDir string) error {
|
||||
goCtx := context.Background()
|
||||
log.Infof("Verifying connectivity to Neo4J at %s", i.Uri)
|
||||
if err := i.Driver.VerifyConnectivity(goCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer i.Driver.Close(goCtx)
|
||||
var err error
|
||||
|
||||
// Get the label to model map
|
||||
|
||||
// Create constraints and indexes
|
||||
// log.Info("Creating constraints and indexes for labels")
|
||||
// for label := range models.NodeLabelToNodeMap {
|
||||
// for _, query := range []string{CreateConstraintQueryTemplate, CreateIndexQueryTemplate} {
|
||||
// _, err := neo4j.ExecuteQuery(goCtx, i.Driver, fmt.Sprintf(query, label), nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
|
||||
// if err != nil {
|
||||
// log.Error(err)
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Process the files in the output directory
|
||||
fileWg := new(sync.WaitGroup)
|
||||
filepath.Walk(graphOutputDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if graphOutputDir == path {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileWg.Add(1)
|
||||
go func(path string) {
|
||||
defer fileWg.Done()
|
||||
i.ProcessFile(path, info)
|
||||
log.Infof("Finished processing file: %s", info.Name())
|
||||
}(path)
|
||||
return nil
|
||||
})
|
||||
fileWg.Wait()
|
||||
log.Info("Finished processing files")
|
||||
|
||||
// Run the post processing merge query
|
||||
log.Info("Running post processing merge query")
|
||||
_, err = neo4j.ExecuteQuery(goCtx, i.Driver, PostProcessMergeQueryTemplate, nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package models
|
||||
|
||||
import "github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
|
||||
|
||||
type Account struct {
|
||||
Id string
|
||||
Arn string
|
||||
Email string
|
||||
Name string
|
||||
Status string
|
||||
JoinedMethod string
|
||||
JoinedTimestamp string
|
||||
IsOrgMgmt bool
|
||||
IsChildAccount bool
|
||||
OrgMgmtAccountID string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
func (a *Account) MakeRelationships() []schema.Relationship {
|
||||
var relationships []schema.Relationship
|
||||
|
||||
// make relationship from children accounts to parent org
|
||||
if a.IsChildAccount {
|
||||
// make relationship from child to org mgmt account
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: a.OrganizationID,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.Organization,
|
||||
TargetLabel: schema.Account,
|
||||
RelationshipType: schema.Manages,
|
||||
})
|
||||
// make relationship from parent org mgmt account to child account
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: a.Id,
|
||||
TargetNodeID: a.OrganizationID,
|
||||
SourceLabel: schema.Account,
|
||||
TargetLabel: schema.Organization,
|
||||
RelationshipType: schema.MemberOf,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
|
||||
)
|
||||
|
||||
var NodeLabelToNodeMap = map[schema.NodeLabel]schema.Node{
|
||||
schema.Organization: &Organization{},
|
||||
schema.Account: &Account{},
|
||||
schema.Role: &Role{},
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
|
||||
)
|
||||
|
||||
type Organization struct {
|
||||
Id string
|
||||
OrgId string
|
||||
Arn string
|
||||
MasterAccountArn string
|
||||
MasterAccountId string
|
||||
MasterAccountEmail string
|
||||
ChildAccounts []Account
|
||||
MgmtAccount Account
|
||||
}
|
||||
|
||||
func (o *Organization) MakeRelationships() []schema.Relationship {
|
||||
return []schema.Relationship{}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
type Resource struct {
|
||||
ARN string
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/BishopFox/cloudfox/internal/common"
|
||||
"github.com/dominikbraun/graph"
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
Id string
|
||||
AccountID string
|
||||
ARN string
|
||||
Name string
|
||||
TrustsDoc policy.TrustPolicyDocument
|
||||
TrustedPrincipals []TrustedPrincipal
|
||||
TrustedServices []TrustedService
|
||||
TrustedFederatedProviders []TrustedFederatedProvider
|
||||
CanPrivEscToAdmin string
|
||||
IsAdmin string
|
||||
IdValue string
|
||||
IsAdminP bool
|
||||
PathToAdminSameAccount bool
|
||||
PathToAdminCrossAccount bool
|
||||
}
|
||||
|
||||
type TrustedPrincipal struct {
|
||||
TrustedPrincipal string
|
||||
ExternalID string
|
||||
VendorName string
|
||||
//IsAdmin bool
|
||||
//CanPrivEscToAdmin bool
|
||||
}
|
||||
|
||||
type TrustedService struct {
|
||||
TrustedService string
|
||||
AccountID string
|
||||
//IsAdmin bool
|
||||
//CanPrivEscToAdmin bool
|
||||
}
|
||||
|
||||
type TrustedFederatedProvider struct {
|
||||
TrustedFederatedProvider string
|
||||
ProviderShortName string
|
||||
TrustedSubjects string
|
||||
//IsAdmin bool
|
||||
//CanPrivEscToAdmin bool
|
||||
}
|
||||
|
||||
func (a *Role) MakeRelationships() []schema.Relationship {
|
||||
var relationships []schema.Relationship
|
||||
//instance := singleton.GetInstance()
|
||||
|
||||
// get thisAccount id from role arn
|
||||
var thisAccount string
|
||||
if len(a.ARN) >= 25 {
|
||||
thisAccount = a.ARN[13:25]
|
||||
} else {
|
||||
fmt.Sprintf("Could not get account number from this role arn%s", a.ARN)
|
||||
}
|
||||
|
||||
// make a relationship between each role and the account it belongs to
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: a.Id,
|
||||
TargetNodeID: thisAccount,
|
||||
SourceLabel: schema.Role,
|
||||
TargetLabel: schema.Account,
|
||||
RelationshipType: schema.MemberOf,
|
||||
})
|
||||
|
||||
for _, TrustedPrincipal := range a.TrustedPrincipals {
|
||||
//get account id from the trusted principal arn
|
||||
var trustedPrincipalAccount string
|
||||
if len(TrustedPrincipal.TrustedPrincipal) >= 25 {
|
||||
trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25]
|
||||
} else {
|
||||
fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal)
|
||||
}
|
||||
var PermissionsRowAccount string
|
||||
// make a TRUSTED_BY relationship between the role and the trusted principal. This does not mean the principal can assume this role, we need more logic to determine that (see below)
|
||||
// relationships = append(relationships, schema.Relationship{
|
||||
// SourceNodeID: TrustedPrincipal.TrustedPrincipal,
|
||||
// TargetNodeID: a.Id,
|
||||
// SourceLabel: schema.Principal,
|
||||
// TargetLabel: schema.Role,
|
||||
// RelationshipType: schema.IsTrustedBy,
|
||||
// })
|
||||
// // make a TRUSTS relationship between the trusted principal and this role. This does not mean the principal can assume this role, we need more logic to determine that (see below)
|
||||
// relationships = append(relationships, schema.Relationship{
|
||||
// SourceNodeID: a.Id,
|
||||
// TargetNodeID: TrustedPrincipal.TrustedPrincipal,
|
||||
// SourceLabel: schema.Role,
|
||||
// TargetLabel: schema.Principal,
|
||||
// RelationshipType: schema.Trusts,
|
||||
// })
|
||||
// // make a MEMBER_OF relationship between the role and the account
|
||||
// relationships = append(relationships, schema.Relationship{
|
||||
// SourceNodeID: TrustedPrincipal.TrustedPrincipal,
|
||||
// TargetNodeID: trustedPrincipalAccount,
|
||||
// SourceLabel: schema.Principal,
|
||||
// TargetLabel: schema.Account,
|
||||
// RelationshipType: schema.MemberOf,
|
||||
// })
|
||||
|
||||
// if the role trusts a principal in this same account explicitly, then the principal can assume the role
|
||||
if thisAccount == trustedPrincipalAccount {
|
||||
// make a CAN_ASSUME relationship between the trusted principal and this role
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.Principal,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAssume,
|
||||
})
|
||||
|
||||
// make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
|
||||
// relationships = append(relationships, schema.Relationship{
|
||||
// SourceNodeID: a.Id,
|
||||
// TargetNodeID: TrustedPrincipal.TrustedPrincipal,
|
||||
// SourceLabel: schema.Role,
|
||||
// TargetLabel: schema.Principal,
|
||||
// RelationshipType: schema.CanBeAssumedBy,
|
||||
// })
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) {
|
||||
// iterate over all rows in AllPermissionsRows
|
||||
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
|
||||
// but we only care about the rows that have arns that are in this account
|
||||
|
||||
if len(PermissionsRow.Arn) >= 25 {
|
||||
PermissionsRowAccount = PermissionsRow.Arn[13:25]
|
||||
} else {
|
||||
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
|
||||
}
|
||||
|
||||
if PermissionsRowAccount == thisAccount {
|
||||
// lets only look for rows that have sts:AssumeRole permissions
|
||||
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
|
||||
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
|
||||
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
|
||||
// if the resource is * or the resource is this role arn, then this principal can assume this role
|
||||
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
|
||||
// make a CAN_ASSUME relationship between the trusted principal and this role
|
||||
//evaluate if the princiapl is a user or a role and set a variable accordingly
|
||||
//var principalType schema.NodeLabel
|
||||
if strings.EqualFold(PermissionsRow.Type, "User") {
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: PermissionsRow.Arn,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.User,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAssume,
|
||||
})
|
||||
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: PermissionsRow.Arn,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.Role,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAssume,
|
||||
})
|
||||
}
|
||||
|
||||
// relationships = append(relationships, schema.Relationship{
|
||||
// SourceNodeID: PermissionsRow.Arn,
|
||||
// TargetNodeID: a.Id,
|
||||
// SourceLabel: principalType,
|
||||
// TargetLabel: schema.Role,
|
||||
// RelationshipType: schema.CanAssumeTest,
|
||||
// })
|
||||
// make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
|
||||
// relationships = append(relationships, schema.Relationship{
|
||||
// SourceNodeID: a.Id,
|
||||
// TargetNodeID: PermissionsRow.Arn,
|
||||
// SourceLabel: schema.Role,
|
||||
// TargetLabel: schema.Principal,
|
||||
// RelationshipType: schema.CanBeAssumedByTest,
|
||||
// })
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) {
|
||||
// iterate over all rows in AllPermissionsRows
|
||||
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
|
||||
// but we only care about the rows that have arns that are in this other account
|
||||
if len(PermissionsRow.Arn) >= 25 {
|
||||
PermissionsRowAccount = PermissionsRow.Arn[13:25]
|
||||
} else {
|
||||
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 strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
|
||||
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
|
||||
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
|
||||
// if the resource is * or the resource is this role arn, then this principal can assume this role
|
||||
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
|
||||
// make a CAN_ASSUME relationship between the trusted principal and this role
|
||||
|
||||
if strings.EqualFold(PermissionsRow.Type, "User") {
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: PermissionsRow.Arn,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.User,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAssumeCrossAccount,
|
||||
})
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: PermissionsRow.Arn,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.User,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAccess,
|
||||
})
|
||||
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: PermissionsRow.Arn,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.Role,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAssumeCrossAccount,
|
||||
})
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: PermissionsRow.Arn,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.Role,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAccess,
|
||||
})
|
||||
}
|
||||
// // make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
|
||||
// relationships = append(relationships, schema.Relationship{
|
||||
// SourceNodeID: a.Id,
|
||||
// TargetNodeID: PermissionsRow.Arn,
|
||||
// SourceLabel: schema.Role,
|
||||
// TargetLabel: schema.Principal,
|
||||
// RelationshipType: schema.CanBeAssumedByTest,
|
||||
// })
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node
|
||||
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" {
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.Vendor,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAssume,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, TrustedService := range a.TrustedServices {
|
||||
// make relationship from trusted service to this role of type can assume
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: TrustedService.TrustedService + "_" + TrustedService.AccountID,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.Service,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.IsTrustedBy,
|
||||
})
|
||||
// make relationship from this role to trusted service of type can be assumed by
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: a.Id,
|
||||
TargetNodeID: TrustedService.TrustedService + "_" + TrustedService.AccountID,
|
||||
SourceLabel: schema.Role,
|
||||
TargetLabel: schema.Service,
|
||||
RelationshipType: schema.Trusts,
|
||||
})
|
||||
}
|
||||
|
||||
for _, TrustedFederatedProvider := range a.TrustedFederatedProviders {
|
||||
// make relationship from trusted federated provider to this role of type can assume
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.FederatedIdentity,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAssume,
|
||||
})
|
||||
// make relationship from this role to trusted federated provider of type can be assumed by
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: a.Id,
|
||||
TargetNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
|
||||
SourceLabel: schema.Role,
|
||||
TargetLabel: schema.FederatedIdentity,
|
||||
RelationshipType: schema.CanBeAssumedBy,
|
||||
})
|
||||
// make relationship from trusted federated provider to this role of type can assume
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.FederatedIdentity,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.IsTrustedBy,
|
||||
})
|
||||
// make relationship from this role to trusted federated provider of type can be assumed by
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: a.Id,
|
||||
TargetNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
|
||||
SourceLabel: schema.Role,
|
||||
TargetLabel: schema.FederatedIdentity,
|
||||
RelationshipType: schema.Trusts,
|
||||
})
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
// func (a *Node) GenerateAttributes() map[string]string {
|
||||
// attributes := make(map[string]string)
|
||||
// attributes["Id"] = a.Id
|
||||
// attributes["Name"] = a.Name
|
||||
// attributes["Type"] = "Role"
|
||||
// attributes["AccountID"] = a.AccountID
|
||||
// attributes["ARN"] = a.ARN
|
||||
// attributes["CanPrivEscToAdmin"] = a.CanPrivEscToAdmin
|
||||
// attributes["IsAdmin"] = a.IsAdmin
|
||||
// attributes["IdValue"] = a.IdValue
|
||||
// return attributes
|
||||
// }
|
||||
|
||||
// func (a *Role) MergeAttributes(newAttributes map[string]string) {
|
||||
// if a.Id == "" {
|
||||
// a.Id = newAttributes["Id"]
|
||||
// }
|
||||
// if a.Name == "" {
|
||||
// a.Name = newAttributes["Name"]
|
||||
// }
|
||||
// if a.Type == "" {
|
||||
// a.Type = newAttributes["Type"]
|
||||
// }
|
||||
// if a.AccountID == "" {
|
||||
// a.AccountID = newAttributes["AccountID"]
|
||||
// }
|
||||
// if a.ARN == "" {
|
||||
// a.ARN = newAttributes["ARN"]
|
||||
// }
|
||||
// if a.CanPrivEscToAdmin == "" {
|
||||
// a.CanPrivEscToAdmin = newAttributes["CanPrivEscToAdmin"]
|
||||
// }
|
||||
// if a.IsAdmin == "" {
|
||||
// a.IsAdmin = newAttributes["IsAdmin"]
|
||||
// }
|
||||
// if a.IdValue == "" {
|
||||
// a.IdValue = newAttributes["IdValue"]
|
||||
// }
|
||||
// }
|
||||
|
||||
func (a *Role) MakeVertices(GlobalGraph graph.Graph[string, string]) {
|
||||
|
||||
// make a vertex for this role as populate all of the data in the Role struct as attributes
|
||||
err := GlobalGraph.AddVertex(
|
||||
a.Id,
|
||||
graph.VertexAttribute("Name", a.Name),
|
||||
graph.VertexAttribute("Type", "Role"),
|
||||
graph.VertexAttribute("AccountID", a.AccountID),
|
||||
graph.VertexAttribute("ARN", a.ARN),
|
||||
graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin),
|
||||
graph.VertexAttribute("IsAdmin", a.IsAdmin),
|
||||
graph.VertexAttribute("IdValue", a.IdValue),
|
||||
)
|
||||
if err != nil {
|
||||
if err == graph.ErrVertexAlreadyExists {
|
||||
fmt.Println(a.Id + " already exists")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// func MakeAllVertices(GlobalRoles []Role, GlobalPmapperGraph aws.PmapperModule) (GlobalGraph graph.Graph[string, string]) {
|
||||
|
||||
// // for all nodes in the GlobalPmapperGraph, check to see if they exist in the GlobalRoles slice. If they do, then update the node with the new data. If they don't, then add the node to the GlobalRoles slice
|
||||
// for _, node := range GlobalPmapperGraph.Nodes {
|
||||
// // check to see if the node exists in the GlobalRoles slice
|
||||
// var nodeExists bool
|
||||
// for _, role := range GlobalRoles {
|
||||
// if node.Id == role.Id {
|
||||
// nodeExists = true
|
||||
// }
|
||||
// }
|
||||
|
||||
// // // make a vertex for this role as populate all of the data in the Role struct as attributes
|
||||
// // err := GlobalGraph.AddVertex(
|
||||
// // a.Id,
|
||||
// // graph.VertexAttribute("Name", a.Name),
|
||||
// // graph.VertexAttribute("Type", "Role"),
|
||||
// // graph.VertexAttribute("AccountID", a.AccountID),
|
||||
// // graph.VertexAttribute("ARN", a.ARN),
|
||||
// // graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin),
|
||||
// // graph.VertexAttribute("IsAdmin", a.IsAdmin),
|
||||
// // graph.VertexAttribute("IdValue", a.IdValue),
|
||||
// // )
|
||||
// // if err != nil {
|
||||
// // if err == graph.ErrVertexAlreadyExists {
|
||||
// // fmt.Println(a.Id + " already exists")
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// }
|
||||
|
||||
func (a *Role) MakeEdges(GlobalGraph graph.Graph[string, string]) []schema.Relationship {
|
||||
var relationships []schema.Relationship
|
||||
|
||||
// get thisAccount id from role arn
|
||||
var thisAccount string
|
||||
if len(a.ARN) >= 25 {
|
||||
thisAccount = a.ARN[13:25]
|
||||
} else {
|
||||
fmt.Sprintf("Could not get account number from this role arn%s", a.ARN)
|
||||
}
|
||||
|
||||
for _, TrustedPrincipal := range a.TrustedPrincipals {
|
||||
//get account id from the trusted principal arn
|
||||
var trustedPrincipalAccount string
|
||||
if len(TrustedPrincipal.TrustedPrincipal) >= 25 {
|
||||
trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25]
|
||||
} else {
|
||||
fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal)
|
||||
}
|
||||
var PermissionsRowAccount string
|
||||
|
||||
// if the role trusts a principal in this same 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.Id,
|
||||
graph.EdgeAttribute("AssumeRole", "Same account explicit trust"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Id + "Same account explicit trust")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) {
|
||||
err := GlobalGraph.AddVertex(
|
||||
a.Id,
|
||||
graph.VertexAttribute("Name", a.Name),
|
||||
graph.VertexAttribute("Type", "Account"),
|
||||
graph.VertexAttribute("AccountID", a.AccountID),
|
||||
)
|
||||
if err != nil {
|
||||
if err == graph.ErrVertexAlreadyExists {
|
||||
fmt.Println(a.Id + " already exists")
|
||||
}
|
||||
}
|
||||
// iterate over all rows in AllPermissionsRows
|
||||
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
|
||||
// but we only care about the rows that have arns that are in this account
|
||||
|
||||
if len(PermissionsRow.Arn) >= 25 {
|
||||
PermissionsRowAccount = PermissionsRow.Arn[13:25]
|
||||
} else {
|
||||
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
|
||||
}
|
||||
|
||||
if PermissionsRowAccount == thisAccount {
|
||||
// lets only look for rows that have sts:AssumeRole permissions
|
||||
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
|
||||
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
|
||||
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
|
||||
// if the resource is * or the resource is this role arn, then this principal can assume this role
|
||||
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
|
||||
// make a CAN_ASSUME relationship between the trusted principal and this role
|
||||
//evaluate if the princiapl is a user or a role and set a variable accordingly
|
||||
//var principalType schema.NodeLabel
|
||||
if strings.EqualFold(PermissionsRow.Type, "User") {
|
||||
err := GlobalGraph.AddEdge(
|
||||
PermissionsRow.Arn,
|
||||
a.Id,
|
||||
graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(PermissionsRow.Arn + a.Id + "Same account root trust and trusted principal has permission to assume role")
|
||||
}
|
||||
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
|
||||
err := GlobalGraph.AddEdge(
|
||||
PermissionsRow.Arn,
|
||||
a.Id,
|
||||
graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(PermissionsRow.Arn + a.Id + "Same account root trust and trusted principal has permission to assume role")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" {
|
||||
err := GlobalGraph.AddVertex(
|
||||
a.Id,
|
||||
graph.VertexAttribute("Name", a.Name),
|
||||
graph.VertexAttribute("Type", "Account"),
|
||||
graph.VertexAttribute("AccountID", a.AccountID),
|
||||
graph.VertexAttribute("VendorName", TrustedPrincipal.VendorName),
|
||||
)
|
||||
if err != nil {
|
||||
if err == graph.ErrVertexAlreadyExists {
|
||||
fmt.Println(a.Id + " already exists")
|
||||
}
|
||||
}
|
||||
// If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
|
||||
TargetNodeID: a.Id,
|
||||
SourceLabel: schema.Vendor,
|
||||
TargetLabel: schema.Role,
|
||||
RelationshipType: schema.CanAssume,
|
||||
})
|
||||
err = GlobalGraph.AddEdge(
|
||||
//TrustedPrincipal.TrustedPrincipal,
|
||||
TrustedPrincipal.VendorName,
|
||||
a.Id,
|
||||
graph.EdgeAttribute("VendorAssumeRole", "Cross account root trust and trusted principal is a vendor"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(TrustedPrincipal.VendorName + a.Id + "Cross account root trust and trusted principal is a vendor")
|
||||
|
||||
}
|
||||
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) {
|
||||
err := GlobalGraph.AddVertex(
|
||||
a.Id,
|
||||
graph.VertexAttribute("Name", a.Name),
|
||||
graph.VertexAttribute("Type", "Account"),
|
||||
graph.VertexAttribute("AccountID", a.AccountID),
|
||||
)
|
||||
if err != nil {
|
||||
if err == graph.ErrVertexAlreadyExists {
|
||||
fmt.Println(a.Id + " already exists")
|
||||
}
|
||||
}
|
||||
// iterate over all rows in AllPermissionsRows
|
||||
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
|
||||
// but we only care about the rows that have arns that are in this other account
|
||||
if len(PermissionsRow.Arn) >= 25 {
|
||||
PermissionsRowAccount = PermissionsRow.Arn[13:25]
|
||||
} else {
|
||||
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 strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
|
||||
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
|
||||
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
|
||||
// if the resource is * or the resource is this role arn, then this principal can assume this role
|
||||
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
|
||||
// make a CAN_ASSUME relationship between the trusted principal and this role
|
||||
|
||||
if strings.EqualFold(PermissionsRow.Type, "User") {
|
||||
err := GlobalGraph.AddEdge(
|
||||
PermissionsRow.Arn,
|
||||
a.Id,
|
||||
graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(PermissionsRow.Arn + a.Id + "Cross account root trust and trusted principal has permission to assume role")
|
||||
}
|
||||
|
||||
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
|
||||
err := GlobalGraph.AddEdge(
|
||||
PermissionsRow.Arn,
|
||||
a.Id,
|
||||
graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(PermissionsRow.Arn + a.Id + "Cross account root trust and trusted principal has permission to assume role")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// pmapper takes care of this part so commenting out for now - but leaving as a placeholder
|
||||
|
||||
// for _, TrustedService := range a.TrustedServices {
|
||||
// // make relationship from trusted service to this role of type can assume
|
||||
// // make relationship from this role to trusted service of type can be assumed by
|
||||
// }
|
||||
|
||||
for _, TrustedFederatedProvider := range a.TrustedFederatedProviders {
|
||||
// make relationship from trusted federated provider to this role of type can assume
|
||||
|
||||
GlobalGraph.AddVertex(
|
||||
TrustedFederatedProvider.TrustedFederatedProvider,
|
||||
graph.VertexAttribute("Type", "FederatedIdentity"),
|
||||
)
|
||||
err := GlobalGraph.AddEdge(
|
||||
TrustedFederatedProvider.TrustedFederatedProvider,
|
||||
a.Id,
|
||||
graph.EdgeAttribute("FederatedAssumeRole", "Trusted federated provider"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Id + "Trusted federated provider")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id string
|
||||
ARN string
|
||||
Name string
|
||||
IsAdmin string
|
||||
CanPrivEscToAdmin string
|
||||
IdValue string
|
||||
}
|
||||
|
||||
func (a *User) MakeRelationships() []schema.Relationship {
|
||||
// make a relationship between each role and the account it belongs to
|
||||
relationships := []schema.Relationship{}
|
||||
|
||||
// get thisAccount id from user arn
|
||||
var thisAccount string
|
||||
if len(a.ARN) >= 25 {
|
||||
thisAccount = a.ARN[13:25]
|
||||
} else {
|
||||
fmt.Sprintf("Could not get account number from this user arn%s", a.ARN)
|
||||
}
|
||||
|
||||
relationships = append(relationships, schema.Relationship{
|
||||
SourceNodeID: a.Id,
|
||||
TargetNodeID: thisAccount,
|
||||
SourceLabel: schema.User,
|
||||
TargetLabel: schema.Account,
|
||||
RelationshipType: schema.MemberOf,
|
||||
Properties: map[string]interface{}{},
|
||||
})
|
||||
return relationships
|
||||
}
|
||||
|
||||
func (a *User) GenerateAttributes() map[string]string {
|
||||
return map[string]string{
|
||||
"Id": a.Id,
|
||||
"ARN": a.ARN,
|
||||
"Name": a.Name,
|
||||
"IsAdmin": a.IsAdmin,
|
||||
"CanPrivEscToAdmin": a.CanPrivEscToAdmin,
|
||||
"IdValue": a.IdValue,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *User) MergeAttributes(other map[string]string) {
|
||||
if other["Id"] != "" {
|
||||
a.Id = other["Id"]
|
||||
}
|
||||
if other["ARN"] != "" {
|
||||
a.ARN = other["ARN"]
|
||||
}
|
||||
if other["Name"] != "" {
|
||||
a.Name = other["Name"]
|
||||
}
|
||||
if other["IsAdmin"] != "" {
|
||||
a.IsAdmin = other["IsAdmin"]
|
||||
}
|
||||
if other["CanPrivEscToAdmin"] != "" {
|
||||
a.CanPrivEscToAdmin = other["CanPrivEscToAdmin"]
|
||||
}
|
||||
if other["IdValue"] != "" {
|
||||
a.IdValue = other["IdValue"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type RelationshipType string
|
||||
type NodeLabel string
|
||||
|
||||
type Node interface {
|
||||
MakeRelationships() []Relationship
|
||||
}
|
||||
|
||||
type Relationship struct {
|
||||
SourceNodeID string `json:"sourceNodeId"`
|
||||
TargetNodeID string `json:"targetNodeId"`
|
||||
SourceLabel NodeLabel `json:"sourceLabel"`
|
||||
TargetLabel NodeLabel `json:"targetLabel"`
|
||||
RelationshipType RelationshipType `json:"relationshipType"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
SourceProperty string `json:"sourceProperty"`
|
||||
TargetProperty string `json:"targetProperty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// Relationships
|
||||
|
||||
AssociatedTo RelationshipType = "AssociatedTo"
|
||||
AttachedTo RelationshipType = "AttachedTo"
|
||||
Authenticates RelationshipType = "Authenticates"
|
||||
ConnectedTo RelationshipType = "ConnectedTo"
|
||||
Contains RelationshipType = "Contains"
|
||||
Exposes RelationshipType = "Exposes"
|
||||
HasAccess RelationshipType = "HasAccess"
|
||||
HasConfig RelationshipType = "HasConfig"
|
||||
HasDisk RelationshipType = "HasDisk"
|
||||
HasInstance RelationshipType = "HasInstance"
|
||||
HasRbac RelationshipType = "HasRbac"
|
||||
HasRole RelationshipType = "HasRole"
|
||||
Manages RelationshipType = "Manages"
|
||||
MemberOf RelationshipType = "MemberOf"
|
||||
Owns RelationshipType = "Owns"
|
||||
Represents RelationshipType = "Represents"
|
||||
Trusts RelationshipType = "Trusts"
|
||||
IsTrustedBy RelationshipType = "IsTrustedBy"
|
||||
CanAssume RelationshipType = "CanAssume"
|
||||
CanAssumeCrossAccount RelationshipType = "CanAssumeCrossAccount"
|
||||
CanBeAssumedBy RelationshipType = "CanBeAssumedBy"
|
||||
CanBeAssumedByTest RelationshipType = "CanBeAssumedByTest"
|
||||
CanAssumeTest RelationshipType = "CanAssumeTest"
|
||||
CanAccess RelationshipType = "CAN_ACCESS"
|
||||
)
|
||||
|
||||
const (
|
||||
// Node labels
|
||||
Resource NodeLabel = "Resource"
|
||||
Account NodeLabel = "Account"
|
||||
Organization NodeLabel = "Org"
|
||||
Service NodeLabel = "Service"
|
||||
Principal NodeLabel = "Principal"
|
||||
Vendor NodeLabel = "Vendor"
|
||||
Role NodeLabel = "Role"
|
||||
Group NodeLabel = "Group"
|
||||
User NodeLabel = "User"
|
||||
FederatedIdentity NodeLabel = "FederatedIdentity"
|
||||
)
|
||||
|
||||
func AsNeo4j(object *Node) map[string]interface{} {
|
||||
|
||||
// We don't want to include these fields in the map
|
||||
fieldsToExclude := []string{"members", "owners", "appRoles", "registeredUsers"}
|
||||
objectMap, err := json.Marshal(object)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var objectMapInterface map[string]interface{}
|
||||
json.Unmarshal(objectMap, &objectMapInterface)
|
||||
for _, field := range fieldsToExclude {
|
||||
delete(objectMapInterface, field)
|
||||
}
|
||||
|
||||
// We need to convert flatten maps to an array
|
||||
// We'll want to keep order of the keys for things like extensionAttributes
|
||||
for key, value := range objectMapInterface {
|
||||
_, isMap := value.(map[string]interface{})
|
||||
if isMap {
|
||||
var valueArray []string
|
||||
var keys []string
|
||||
|
||||
for k := range value.(map[string]interface{}) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
valueString := value.(map[string]interface{})[k].(string)
|
||||
if valueString != "" {
|
||||
valueArray = append(valueArray, k, valueString)
|
||||
}
|
||||
}
|
||||
objectMapInterface[key] = valueArray
|
||||
}
|
||||
}
|
||||
return objectMapInterface
|
||||
}
|
||||
|
||||
func ConvertCustomTypesToNeo4j(node interface{}) (map[string]interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
val := reflect.ValueOf(node)
|
||||
|
||||
// Handling pointers to structs or interfaces
|
||||
for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
// Check if the value is valid and if it's a struct
|
||||
if !val.IsValid() || val.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("invalid input: not a struct or a pointer to a struct")
|
||||
}
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := val.Type().Field(i) // Get the StructField
|
||||
|
||||
// Check if the field is a struct or slice of structs and not one of the basic types
|
||||
if (fieldType.Type.Kind() == reflect.Struct ||
|
||||
(fieldType.Type.Kind() == reflect.Slice && fieldType.Type.Elem().Kind() == reflect.Struct)) &&
|
||||
fieldType.Type != reflect.TypeOf([]string{}) &&
|
||||
fieldType.Type != reflect.TypeOf([]int{}) &&
|
||||
fieldType.Type != reflect.TypeOf([]bool{}) {
|
||||
// Convert complex field to JSON string
|
||||
jsonStr, err := json.Marshal(field.Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[fieldType.Name] = string(jsonStr)
|
||||
} else {
|
||||
// Directly use the field for primitive types
|
||||
result[fieldType.Name] = field.Interface()
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
+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
|
||||
}
|
||||
|
||||
|
||||
+30
-17
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -24,17 +23,19 @@ import (
|
||||
|
||||
type InstancesModule struct {
|
||||
// General configuration data
|
||||
EC2Client sdk.AWSEC2ClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
EC2Client sdk.AWSEC2ClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
PmapperDataBasePath string
|
||||
|
||||
Goroutines int
|
||||
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
|
||||
@@ -94,8 +95,8 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo
|
||||
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
|
||||
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -127,9 +128,13 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Perform role analysis
|
||||
// Signal the receiver to stop and wait for it to finish draining the channel.
|
||||
// This must happen BEFORE role analysis to ensure all MappedInstances are collected.
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// Perform role analysis (all instances are now in m.MappedInstances)
|
||||
if m.pmapperError == nil {
|
||||
for i := range m.MappedInstances {
|
||||
m.MappedInstances[i].Admin, m.MappedInstances[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.MappedInstances[i].Role)
|
||||
@@ -143,8 +148,6 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// This conditional block will either dump the userData attribute content or the general instances data, depending on what you select via command line.
|
||||
//fmt.Printf("\n[*] Preparing output...\n\n")
|
||||
@@ -434,13 +437,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--
|
||||
@@ -448,6 +459,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,7 +549,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)
|
||||
}
|
||||
|
||||
|
||||
+18
-11
@@ -31,13 +31,16 @@ type LambdasModule struct {
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
PmapperDataBasePath string
|
||||
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
// Main module data
|
||||
Lambdas []Lambda
|
||||
@@ -75,8 +78,8 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating lambdas for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
|
||||
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -226,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
|
||||
@@ -50,6 +51,7 @@ type OutboundAssumeRoleEntry struct {
|
||||
SourcePrincipal string
|
||||
DestinationAccount string
|
||||
DestinationPrincipal string
|
||||
Action string
|
||||
LogTimestamp string
|
||||
}
|
||||
|
||||
@@ -117,6 +119,24 @@ type CloudTrailEvent struct {
|
||||
} `json:"tlsDetails"`
|
||||
}
|
||||
|
||||
var interestingCrossAccountEventNames = []string{
|
||||
"AssumeRole",
|
||||
"AssumeRoleWithSAML",
|
||||
"AssumeRoleWithWebIdentity",
|
||||
"GetObject",
|
||||
"ListBuckets",
|
||||
"BatchGetImage",
|
||||
"GetDownloadUrlForLayer",
|
||||
"SendMessage",
|
||||
"GetQueueUrl",
|
||||
"Invoke20150331",
|
||||
"RunInstances",
|
||||
"RunTask",
|
||||
"StartTask",
|
||||
"CreateTask",
|
||||
"CreateTaskSet",
|
||||
}
|
||||
|
||||
func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
@@ -171,6 +191,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
|
||||
"Source Principal",
|
||||
//"Destination Account",
|
||||
"Destination Principal",
|
||||
"Action",
|
||||
"Log Entry Timestamp",
|
||||
}
|
||||
|
||||
@@ -194,6 +215,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
|
||||
"Source Principal",
|
||||
//"Destination Account",
|
||||
"Destination Principal",
|
||||
"Action",
|
||||
"Log Entry Timestamp",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
@@ -206,6 +228,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
|
||||
"Source Principal",
|
||||
//"Destination Account",
|
||||
"Destination Principal",
|
||||
"Action",
|
||||
"Log Entry Timestamp",
|
||||
}
|
||||
}
|
||||
@@ -222,6 +245,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
|
||||
m.OutboundAssumeRoleEntries[i].SourcePrincipal,
|
||||
//m.OutboundAssumeRoleEntries[i].DestinationAccount,
|
||||
m.OutboundAssumeRoleEntries[i].DestinationPrincipal,
|
||||
m.OutboundAssumeRoleEntries[i].Action,
|
||||
m.OutboundAssumeRoleEntries[i].LogTimestamp,
|
||||
},
|
||||
)
|
||||
@@ -271,17 +295,24 @@ 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 {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
// wg.Add(1)
|
||||
// m.CommandCounter.Total++
|
||||
// m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Total++
|
||||
m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
m.getCrossAccountBatchGetImageEntriesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -359,6 +390,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
|
||||
SourcePrincipal: sourcePrincipal,
|
||||
DestinationAccount: destinationAccount,
|
||||
DestinationPrincipal: destinationPrincipal,
|
||||
Action: "sts:AssumeRole",
|
||||
LogTimestamp: logTimestamp,
|
||||
}
|
||||
}
|
||||
@@ -376,3 +408,107 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// get cross account batch get image entries
|
||||
func (m *OutboundAssumedRolesModule) getCrossAccountBatchGetImageEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
//var LookupAttributes []types.LookupAttributes
|
||||
//var LookupAttribute types.LookupAttribute
|
||||
var pages int
|
||||
|
||||
days := 0 - m.Days
|
||||
endTime := aws.Time(time.Now())
|
||||
startTime := endTime.AddDate(0, 0, days)
|
||||
for {
|
||||
LookupEvents, err := m.CloudTrailClient.LookupEvents(
|
||||
context.TODO(),
|
||||
&cloudtrail.LookupEventsInput{
|
||||
EndTime: endTime,
|
||||
StartTime: &startTime,
|
||||
// LookupAttributes: []cloudtrailTypes.LookupAttribute{
|
||||
// {
|
||||
// AttributeKey: cloudtrailTypes.LookupAttributeKeyEventName,
|
||||
// AttributeValue: aws.String("BatchGetImage"),
|
||||
// },
|
||||
// },
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
|
||||
func(o *cloudtrail.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, event := range LookupEvents.Events {
|
||||
//eventData := *event.CloudTrailEvent
|
||||
//fmt.Println(eventData)
|
||||
var sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal, userType string
|
||||
cloudtrailEvent := CloudTrailEvent{}
|
||||
json.Unmarshal([]byte(*event.CloudTrailEvent), &cloudtrailEvent)
|
||||
|
||||
for _, eventName := range interestingCrossAccountEventNames {
|
||||
if aws.ToString(event.EventName) == eventName {
|
||||
|
||||
// extract the source account and principal
|
||||
if cloudtrailEvent.UserIdentity.Type == "AssumedRole" || cloudtrailEvent.UserIdentity.Type == "IAMUser" || cloudtrailEvent.UserIdentity.Type == "Role" {
|
||||
if cloudtrailEvent.UserIdentity.Type == "AssumedRole" {
|
||||
sourcePrincipal = cloudtrailEvent.UserIdentity.SessionContext.SessionIssuer.Arn
|
||||
} else {
|
||||
sourcePrincipal = cloudtrailEvent.UserIdentity.Arn
|
||||
}
|
||||
userType = cloudtrailEvent.UserIdentity.Type
|
||||
sourceAccount = cloudtrailEvent.UserIdentity.AccountID
|
||||
|
||||
//fmt.Printf("%s,%s,%s,%s\n", sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal)
|
||||
|
||||
if cloudtrailEvent.Resources != nil {
|
||||
destinationAccount = cloudtrailEvent.Resources[0].AccountID
|
||||
destinationPrincipal = cloudtrailEvent.Resources[0].Arn
|
||||
if sourceAccount != destinationAccount {
|
||||
logTimestamp := cloudtrailEvent.EventTime.Format("2006-01-02 15:04:05")
|
||||
dataReceiver <- OutboundAssumeRoleEntry{
|
||||
AWSService: "CloudTrail",
|
||||
Region: r,
|
||||
Type: userType,
|
||||
SourceAccount: sourceAccount,
|
||||
SourcePrincipal: sourcePrincipal,
|
||||
DestinationAccount: destinationAccount,
|
||||
DestinationPrincipal: destinationPrincipal,
|
||||
Action: aws.ToString(event.EventName),
|
||||
LogTimestamp: logTimestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if LookupEvents.NextToken != nil {
|
||||
PaginationControl = LookupEvents.NextToken
|
||||
pages++
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+15
-24
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/BishopFox/cloudfox/internal/common"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
@@ -36,7 +37,7 @@ type IamPermissionsModule struct {
|
||||
Users []GAADUser
|
||||
Roles []GAADRole
|
||||
Groups []GAADGroup
|
||||
Rows []PermissionsRow
|
||||
Rows []common.PermissionsRow
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
@@ -71,20 +72,6 @@ type GAADGroup struct {
|
||||
InlinePolicies []types.PolicyDetail
|
||||
}
|
||||
|
||||
type PermissionsRow struct {
|
||||
AWSService string
|
||||
Type string
|
||||
Name string
|
||||
Arn string
|
||||
PolicyType string
|
||||
PolicyName string
|
||||
PolicyArn string
|
||||
Effect string
|
||||
Action string
|
||||
Resource string
|
||||
Condition string
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbosity int, principal string) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
@@ -104,8 +91,8 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
|
||||
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
}
|
||||
|
||||
m.getGAAD()
|
||||
m.parsePermissions(principal)
|
||||
m.GetGAAD()
|
||||
m.ParsePermissions(principal)
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
@@ -206,10 +193,10 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
|
||||
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.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) getGAAD() {
|
||||
func (m *IamPermissionsModule) GetGAAD() {
|
||||
GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.modLog.Error(err)
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
@@ -297,7 +284,7 @@ func (m *IamPermissionsModule) getPrincipalArn(principal string) string {
|
||||
return arn
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) parsePermissions(principal string) {
|
||||
func (m *IamPermissionsModule) ParsePermissions(principal string) {
|
||||
var inputArn string
|
||||
for i := range m.Roles {
|
||||
if principal == "" {
|
||||
@@ -422,6 +409,10 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
|
||||
//parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
|
||||
document, _ := url.QueryUnescape(aws.ToString(d.Document))
|
||||
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
|
||||
|
||||
// hasStsAssumeRole := parsedPolicyDocument.DoesPolicyHaveMatchingStatement("Allow", "sts:AssumeRole", "*")
|
||||
// fmt.Println(hasStsAssumeRole)
|
||||
|
||||
for _, s = range parsedPolicyDocument.Statement {
|
||||
//version := parsedPolicyDocument.Version
|
||||
effect := s.Effect
|
||||
@@ -435,7 +426,7 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
common.PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
@@ -462,7 +453,7 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
common.PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
@@ -508,7 +499,7 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
common.PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
@@ -534,7 +525,7 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
common.PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
|
||||
+354
-23
@@ -1,6 +1,7 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -10,9 +11,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/sts"
|
||||
"github.com/dominikbraun/graph"
|
||||
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -28,15 +31,22 @@ type PmapperModule struct {
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
pmapperGraph graph.Graph[string, string]
|
||||
Nodes []Node
|
||||
Edges []Edge
|
||||
CommandCounter internal.CommandCounter
|
||||
PmapperDataBasePath string
|
||||
pmapperGraph graph.Graph[string, string]
|
||||
Nodes []Node
|
||||
Edges []Edge
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type PmapperOutputRow struct {
|
||||
Start string
|
||||
End string
|
||||
Paths []string
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
@@ -45,19 +55,54 @@ type Edge struct {
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Arn string `json:"arn"`
|
||||
IDValue string `json:"id_value"`
|
||||
AttachedPolicies []AttachedPolicies `json:"attached_policies"`
|
||||
GroupMemberships []interface{} `json:"group_memberships"`
|
||||
TrustPolicy interface{} `json:"trust_policy"`
|
||||
InstanceProfile interface{} `json:"instance_profile"`
|
||||
ActivePassword bool `json:"active_password"`
|
||||
AccessKeys int `json:"access_keys"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
PermissionsBoundary interface{} `json:"permissions_boundary"`
|
||||
HasMfa bool `json:"has_mfa"`
|
||||
Tags Tags `json:"tags"`
|
||||
PathToAdmin bool
|
||||
Arn string `json:"arn"`
|
||||
Type string
|
||||
AccountID string
|
||||
Name string
|
||||
IDValue string `json:"id_value"`
|
||||
AttachedPolicies []AttachedPolicies `json:"attached_policies"`
|
||||
GroupMemberships []interface{} `json:"group_memberships"`
|
||||
TrustPolicy interface{} `json:"trust_policy"`
|
||||
TrustsDoc policy.TrustPolicyDocument
|
||||
TrustedPrincipals []TrustedPrincipal
|
||||
TrustedServices []TrustedService
|
||||
TrustedFederatedProviders []TrustedFederatedProvider
|
||||
InstanceProfile interface{} `json:"instance_profile"`
|
||||
ActivePassword bool `json:"active_password"`
|
||||
AccessKeys int `json:"access_keys"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
PathToAdmin bool
|
||||
PermissionsBoundary interface{} `json:"permissions_boundary"`
|
||||
HasMfa bool `json:"has_mfa"`
|
||||
Tags Tags `json:"tags"`
|
||||
CanPrivEscToAdminString string
|
||||
IsAdminString string
|
||||
VendorName string
|
||||
}
|
||||
|
||||
type TrustedPrincipal struct {
|
||||
TrustedPrincipal string
|
||||
ExternalID string
|
||||
VendorName string
|
||||
AccountIsInAnalyzedAccountList bool
|
||||
//IsAdmin bool
|
||||
//CanPrivEscToAdmin bool
|
||||
}
|
||||
|
||||
type TrustedService struct {
|
||||
TrustedService string
|
||||
AccountID string
|
||||
//IsAdmin bool
|
||||
//CanPrivEscToAdmin bool
|
||||
}
|
||||
|
||||
type TrustedFederatedProvider struct {
|
||||
TrustedFederatedProvider string
|
||||
ProviderAccountId string
|
||||
ProviderShortName string
|
||||
TrustedSubjects []string
|
||||
//IsAdmin bool
|
||||
//CanPrivEscToAdmin bool
|
||||
}
|
||||
|
||||
type AttachedPolicies struct {
|
||||
@@ -79,9 +124,16 @@ func (m *PmapperModule) initPmapperGraph() error {
|
||||
for i := range m.Nodes {
|
||||
if m.doesNodeHavePathToAdmin(m.Nodes[i]) {
|
||||
m.Nodes[i].PathToAdmin = true
|
||||
m.Nodes[i].CanPrivEscToAdminString = "Yes"
|
||||
//fmt.Println(m.Nodes[i].Arn, m.Nodes[i].IsAdmin, m.Nodes[i].PathToAdmin)
|
||||
} else {
|
||||
m.Nodes[i].PathToAdmin = false
|
||||
m.Nodes[i].CanPrivEscToAdminString = "No"
|
||||
}
|
||||
if m.Nodes[i].IsAdmin {
|
||||
m.Nodes[i].IsAdminString = "Yes"
|
||||
} else {
|
||||
m.Nodes[i].IsAdminString = "No"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +159,39 @@ func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] {
|
||||
}
|
||||
|
||||
for _, edge := range m.Edges {
|
||||
_ = pmapperGraph.AddEdge(edge.Source, edge.Destination)
|
||||
err := pmapperGraph.AddEdge(
|
||||
edge.Source,
|
||||
edge.Destination,
|
||||
graph.EdgeAttribute(edge.ShortReason, edge.Reason),
|
||||
)
|
||||
if err != nil {
|
||||
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, but adding a new one!")
|
||||
|
||||
// get the existing edge
|
||||
existingEdge, _ := pmapperGraph.Edge(edge.Source, edge.Destination)
|
||||
// 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[edge.ShortReason] = edge.Reason
|
||||
//Update the edge
|
||||
pmapperGraph.UpdateEdge(
|
||||
edge.Source,
|
||||
edge.Destination,
|
||||
graph.EdgeAttributes(existingProperties.Attributes),
|
||||
)
|
||||
|
||||
}
|
||||
//fmt.Println(edge.Reason)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration)
|
||||
@@ -119,7 +203,11 @@ func (m *PmapperModule) DoesPrincipalHavePathToAdmin(principal string) bool {
|
||||
for i := range m.Nodes {
|
||||
if m.Nodes[i].Arn == principal {
|
||||
if m.Nodes[i].PathToAdmin {
|
||||
m.Nodes[i].CanPrivEscToAdminString = "Yes"
|
||||
return true
|
||||
} else {
|
||||
m.Nodes[i].CanPrivEscToAdminString = "No"
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +219,11 @@ func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool {
|
||||
for i := range m.Nodes {
|
||||
if m.Nodes[i].Arn == principal {
|
||||
if m.Nodes[i].IsAdmin {
|
||||
m.Nodes[i].IsAdminString = "Yes"
|
||||
return true
|
||||
} else {
|
||||
m.Nodes[i].IsAdminString = "No"
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +326,9 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int)
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
Loot: internal.LootClient{
|
||||
DirectoryName: m.output.FilePath,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
@@ -243,7 +338,21 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int)
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
|
||||
header, body := m.createPmapperTableData(outputDirectory)
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: header,
|
||||
Body: body,
|
||||
Name: "pmapper-privesc-paths-enhanced",
|
||||
SkipPrintToScreen: true,
|
||||
})
|
||||
|
||||
loot := m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
|
||||
Name: m.output.CallingModule,
|
||||
Contents: loot,
|
||||
})
|
||||
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
|
||||
fmt.Printf("[%s][%s] %s principals who are admin or have a path to admin identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
@@ -264,9 +373,6 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
|
||||
for _, p := range path {
|
||||
if p != "" {
|
||||
if startNode.Arn != destNode.Arn {
|
||||
// if we got here there is a path
|
||||
//fmt.Printf("%s has a path %s who is an admin.\n", startNode.Arn, destNode.Arn)
|
||||
//fmt.Println(path)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -278,9 +384,136 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *PmapperModule) createPmapperTableData(outputDirectory string) ([]string, [][]string) {
|
||||
var header []string
|
||||
var body [][]string
|
||||
|
||||
header = []string{
|
||||
"Start",
|
||||
"End",
|
||||
"Path(s)",
|
||||
}
|
||||
|
||||
var paths string
|
||||
var admins, privescPathsBody [][]string
|
||||
|
||||
for _, startNode := range m.Nodes {
|
||||
if startNode.IsAdmin {
|
||||
admins = append(admins, []string{startNode.Arn, "", "ADMIN"})
|
||||
|
||||
} else {
|
||||
for _, destNode := range m.Nodes {
|
||||
if destNode.IsAdmin {
|
||||
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
|
||||
// if we have a path,
|
||||
|
||||
if len(path) > 0 {
|
||||
if startNode.Arn != destNode.Arn {
|
||||
paths = ""
|
||||
// if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen
|
||||
for i := 0; i < len(path)-1; i++ {
|
||||
for _, edge := range m.Edges {
|
||||
if edge.Source == path[i] && edge.Destination == path[i+1] {
|
||||
|
||||
//Some pmapper reasons have commas in them so lets get rid of them in the csvOutputdata
|
||||
edge.Reason = strings.ReplaceAll(edge.Reason, ",", " and")
|
||||
paths += fmt.Sprintf("%s %s %s\n", path[i], edge.Reason, path[i+1])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
//trim the last newline from csvPaths
|
||||
paths = strings.TrimSuffix(paths, "\n")
|
||||
privescPathsBody = append(privescPathsBody, []string{startNode.Arn, destNode.Arn, paths})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create body by first adding the admins and then the privesc paths
|
||||
body = append(body, admins...)
|
||||
body = append(body, privescPathsBody...)
|
||||
return header, body
|
||||
|
||||
}
|
||||
|
||||
func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
panic(err.Error())
|
||||
}
|
||||
lootFilePath := filepath.Join(path, "pmapper.txt")
|
||||
|
||||
var admins, out string
|
||||
|
||||
for _, startNode := range m.Nodes {
|
||||
if startNode.IsAdmin {
|
||||
admins += fmt.Sprintf("ADMIN FOUND: %s\n", startNode.Arn)
|
||||
} else {
|
||||
for _, destNode := range m.Nodes {
|
||||
if destNode.IsAdmin {
|
||||
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
|
||||
// if we have a path,
|
||||
|
||||
if len(path) > 0 {
|
||||
if startNode.Arn != destNode.Arn {
|
||||
// if we got here there is a path
|
||||
out += fmt.Sprintf("PATH TO ADMIN FOUND\n Start: %s\n End: %s\n Path(s):\n", startNode.Arn, destNode.Arn)
|
||||
//fmt.Println(path)
|
||||
// if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen
|
||||
for i := 0; i < len(path)-1; i++ {
|
||||
for _, edge := range m.Edges {
|
||||
if edge.Source == path[i] && edge.Destination == path[i+1] {
|
||||
// print it like this: [start node] [reason] [end node]
|
||||
out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1])
|
||||
}
|
||||
// shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice
|
||||
//for _, edge := range GlobalPmapperEdges {
|
||||
// if edge.Source == path[i] && edge.Destination == path[i+1] {
|
||||
// // print it like this: [start node] [reason] [end node]
|
||||
// out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1])
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
out += fmt.Sprintf("\n")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out = admins + "\n\n" + out
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Beginning of loot file"))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file"))
|
||||
}
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), magenta(fmt.Sprintf("Loot file with ALL potential paths written to: [%s]", lootFilePath)))
|
||||
return out
|
||||
|
||||
}
|
||||
|
||||
func (m *PmapperModule) readPmapperData(accountID *string) error {
|
||||
|
||||
e, n := generatePmapperDataBasePaths(accountID)
|
||||
var e, n string
|
||||
if m.PmapperDataBasePath == "" {
|
||||
e, n = generatePmapperDataBasePaths(accountID)
|
||||
} else {
|
||||
e = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "edges.json")
|
||||
n = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "nodes.json")
|
||||
}
|
||||
|
||||
nodesFile, err := os.Open(n)
|
||||
if err != nil {
|
||||
@@ -301,3 +534,101 @@ func (m *PmapperModule) readPmapperData(accountID *string) error {
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (m *PmapperModule) GenerateCypherStatements(goCtx context.Context, driver neo4j.DriverWithContext) error {
|
||||
// Insert nodes
|
||||
for i, node := range m.Nodes {
|
||||
query, params := m.generateNodeCreateStatement(node, i)
|
||||
if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Insert edges
|
||||
for i, edge := range m.Edges {
|
||||
query, params := m.generateEdgeCreateStatement(edge, i)
|
||||
if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PmapperModule) generateNodeCreateStatement(node Node, i int) (string, map[string]interface{}) {
|
||||
var ptype, label, query string
|
||||
var params map[string]any
|
||||
|
||||
if strings.Contains(node.Arn, "role") {
|
||||
label = GetResourceNameFromArn(node.Arn)
|
||||
ptype = "Role"
|
||||
params = map[string]any{
|
||||
"Id": node.Arn,
|
||||
"ARN": node.Arn,
|
||||
"Name": GetResourceNameFromArn(node.Arn),
|
||||
"IdValue": node.IDValue,
|
||||
"IsAdminP": node.IsAdmin,
|
||||
"PathToAdmin": node.PathToAdmin,
|
||||
}
|
||||
|
||||
} else if strings.Contains(node.Arn, "user") {
|
||||
label = GetResourceNameFromArn(node.Arn)
|
||||
ptype = "User"
|
||||
//node.TrustPolicy = ""
|
||||
params = map[string]any{
|
||||
"Id": node.Arn,
|
||||
"ARN": node.Arn,
|
||||
"Name": GetResourceNameFromArn(node.Arn),
|
||||
"IdValue": node.IDValue,
|
||||
"IsAdminP": node.IsAdmin,
|
||||
"PathToAdmin": node.PathToAdmin,
|
||||
}
|
||||
|
||||
} else if strings.Contains(node.Arn, "group") {
|
||||
label = GetResourceNameFromArn(node.Arn)
|
||||
ptype = "Group"
|
||||
}
|
||||
label = strings.ReplaceAll(label, "-", "_")
|
||||
label = strings.ReplaceAll(label, ".", "_")
|
||||
|
||||
query = `MERGE (%s:%s {Id: $Id, ARN: $ARN, Name: $Name, IdValue: $IdValue, IsAdminP: $IsAdminP, PathToAdmin: $PathToAdmin})`
|
||||
|
||||
//sanitizedArn := sanitizeArnForNeo4jLabel(node.Arn)
|
||||
//id := fmt.Sprintf("%s_%s", sanitizedArn, ptype)
|
||||
|
||||
fmt.Println(fmt.Sprintf(query, label, ptype), params)
|
||||
return fmt.Sprintf(query, label, ptype), params
|
||||
}
|
||||
|
||||
func (m *PmapperModule) generateEdgeCreateStatement(edge Edge, i int) (string, map[string]interface{}) {
|
||||
// Sanitize ARNs for matching nodes
|
||||
//srcArnSanitized := sanitizeArnForNeo4jLabel(edge.Source)
|
||||
//destArnSanitized := sanitizeArnForNeo4jLabel(edge.Destination)
|
||||
|
||||
query := `MATCH (a {ARN: $srcArn}), (b {ARN: $destArn}) CREATE (a)-[:CAN_ACCESS {reason: $reason, shortReason: $shortReason}]->(b)`
|
||||
params := map[string]any{
|
||||
"srcArn": edge.Source,
|
||||
"destArn": edge.Destination,
|
||||
"reason": edge.Reason,
|
||||
"shortReason": edge.ShortReason,
|
||||
}
|
||||
fmt.Println(query, params)
|
||||
return query, params
|
||||
}
|
||||
|
||||
func (m *PmapperModule) executeCypherQuery(ctx context.Context, driver neo4j.DriverWithContext, query string, params map[string]interface{}) error {
|
||||
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
|
||||
if err != nil {
|
||||
sharedLogger.Errorf("Error executing query: %s -- %v", err, params)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitizeArnForNeo4jLabel(arn string) string {
|
||||
// Replace non-allowed characters with underscores or other allowed characters
|
||||
sanitized := strings.ReplaceAll(arn, ":", "_")
|
||||
sanitized = strings.ReplaceAll(sanitized, "-", "_")
|
||||
// Add more replacements if needed
|
||||
return sanitized
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-12
@@ -31,8 +31,10 @@ type RoleTrustsModule struct {
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
PmapperDataBasePath string
|
||||
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
// Main module data
|
||||
@@ -83,8 +85,8 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
//fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
|
||||
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
@@ -217,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),
|
||||
@@ -279,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 != "" {
|
||||
@@ -290,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,
|
||||
}
|
||||
@@ -463,11 +466,17 @@ 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!!!")
|
||||
}
|
||||
case strings.Contains(statement.Principal.Federated[0], "oidc.eks"):
|
||||
provider = "EKS"
|
||||
// extract accountId from statement.Principal.Federated[0]
|
||||
accountId := strings.Split(statement.Principal.Federated[0], ":")[4]
|
||||
provider = fmt.Sprintf("EKS-%s", accountId)
|
||||
//provider = "EKS"
|
||||
//provider = statement.Principal.Federated[0]
|
||||
if len(statement.Condition.StringLike.OidcEksSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.OidcEksSub...)
|
||||
} else if len(statement.Condition.StringEquals.OidcEksSub) > 0 {
|
||||
@@ -496,9 +505,11 @@ func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string
|
||||
subjects = append(subjects, "ALL ISSUERS")
|
||||
}
|
||||
|
||||
// AWS SSO case
|
||||
///AWS SSO case
|
||||
case strings.Contains(statement.Principal.Federated[0], "AWSSSO"):
|
||||
provider = "AWS SSO"
|
||||
//provider = "AWS SSO"
|
||||
accountId := strings.Split(statement.Principal.Federated[0], ":")[4]
|
||||
provider = fmt.Sprintf("AWSSSO-%s", accountId)
|
||||
subjects = append(subjects, "Not applicable")
|
||||
|
||||
// okta case
|
||||
@@ -553,6 +564,10 @@ func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string
|
||||
} else {
|
||||
subjects = append(subjects, "ALL PROJECTS")
|
||||
}
|
||||
case strings.Contains(statement.Principal.Federated[0], "saml-provider"):
|
||||
// the provider name is the last part of the ARN
|
||||
provider = strings.Split(statement.Principal.Federated[0], ":saml-provider/")[1]
|
||||
subjects = append(subjects, "Not applicable")
|
||||
|
||||
default:
|
||||
provider = "Unknown Federated Provider"
|
||||
|
||||
+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 {
|
||||
@@ -20,6 +21,7 @@ type CodeBuildClientInterface interface {
|
||||
|
||||
func init() {
|
||||
gob.Register(codeBuildTypes.Project{})
|
||||
gob.Register([]codeBuildTypes.Project{})
|
||||
|
||||
}
|
||||
|
||||
@@ -30,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(
|
||||
@@ -70,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(),
|
||||
@@ -96,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(),
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"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 {
|
||||
DescribeDirectories(context.Context, *directoryservice.DescribeDirectoriesInput, ...func(*directoryservice.Options)) (*directoryservice.DescribeDirectoriesOutput, error)
|
||||
DescribeTrusts(context.Context, *directoryservice.DescribeTrustsInput, ...func(*directoryservice.Options)) (*directoryservice.DescribeTrustsOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]dsTypes.DirectoryDescription{})
|
||||
gob.Register([]dsTypes.Trust{})
|
||||
|
||||
}
|
||||
|
||||
func CachedDSDescribeDirectories(client AWSDSClientInterface, accountID string, region string) ([]dsTypes.DirectoryDescription, error) {
|
||||
var PaginationControl *string
|
||||
var directories []dsTypes.DirectoryDescription
|
||||
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(),
|
||||
&directoryservice.DescribeDirectoriesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *directoryservice.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return directories, err
|
||||
}
|
||||
|
||||
directories = append(directories, DescribeDirectories.DirectoryDescriptions...)
|
||||
|
||||
//pagination
|
||||
if DescribeDirectories.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeDirectories.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, directories, cache.DefaultExpiration)
|
||||
return directories, nil
|
||||
}
|
||||
|
||||
func CachedDSDescribeTrusts(client AWSDSClientInterface, accountID string, region string, directoryId string) ([]dsTypes.Trust, error) {
|
||||
var PaginationControl *string
|
||||
var trusts []dsTypes.Trust
|
||||
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(),
|
||||
&directoryservice.DescribeTrustsInput{
|
||||
DirectoryId: &directoryId,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *directoryservice.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return trusts, err
|
||||
}
|
||||
|
||||
trusts = append(trusts, DescribeDirectoryTrusts.Trusts...)
|
||||
|
||||
//pagination
|
||||
if DescribeDirectoryTrusts.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeDirectoryTrusts.NextToken
|
||||
}
|
||||
internal.Cache.Set(cacheKey, trusts, cache.DefaultExpiration)
|
||||
|
||||
return trusts, nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+13
-12
@@ -77,11 +77,12 @@ func isRoleAdmin(iamSimMod IamSimulatorModule, principal *string) bool {
|
||||
|
||||
}
|
||||
|
||||
func initPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) (PmapperModule, error) {
|
||||
func InitPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int, PmapperDataBasePath string) (PmapperModule, error) {
|
||||
pmapperMod := PmapperModule{
|
||||
Caller: Caller,
|
||||
AWSProfile: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
Caller: Caller,
|
||||
AWSProfile: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
PmapperDataBasePath: PmapperDataBasePath,
|
||||
}
|
||||
err := pmapperMod.initPmapperGraph()
|
||||
if err != nil {
|
||||
@@ -99,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.")
|
||||
}
|
||||
@@ -122,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))
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 266 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user