Compare commits

..
30 Commits
Author SHA1 Message Date
Bastien Faure 599ddc247d Merge main 2023-07-01 17:40:50 -07:00
Bastien Faure 9bedfcb4de Cosmetics 2023-06-28 00:56:52 -07:00
Bastien Faure afbcab6652 Clarified GCP resource selection options in the CLI 2023-06-28 00:50:18 -07:00
Bastien Faure 3ec90f51d2 Improve resource selection logic in GCP by prepopulating Projects and Folders in the client for later use 2023-06-28 00:48:57 -07:00
Bastien Faure 1343060c4b Finish the resource selection logic implementation 2023-06-26 22:01:16 -07:00
Bastien Faure bbc26e0a23 Implement basic support of inventory command 2023-06-26 00:24:33 -07:00
Bastien Faure 8518b7678d Improve buckets module logging 2023-06-26 00:24:11 -07:00
Bastien Faure 07cd08ccc2 Improve data visualization in GCP resource tree 2023-06-25 23:24:08 -07:00
Bastien Faure 4ffc02c285 Implement tree structure in internal package, improved GCP Hierarchy command logic 2023-06-25 23:02:58 -07:00
Bastien Faure f61dd2bfb1 Included orphaned projects in the hierarchy module, improve logging 2023-06-25 16:27:13 -07:00
Bastien Faure 5365206a49 Update README with correct number of available GCP commands 2023-06-25 13:54:26 -07:00
Bastien Faure fc390b763e Fix bug in GCP profile selection logic, remove unnecessary new lines in GCP modules' logging 2023-06-25 13:53:38 -07:00
Bastien Faure a2140f1ef1 Implement definitive logic for GCP Profile interation and module calling 2023-06-25 02:27:51 -07:00
Bastien Faure 48f92dd4f8 Remove GCP development/test command, updated README with usable/current commands 2023-06-25 00:48:48 -07:00
Bastien Faure 139e4d391f Remove GCP development/test command, updated README with usable/current commands 2023-06-25 00:48:05 -07:00
Bastien Faure 6251b76a12 Bypass GCP profile parsing logic for commands that don't require authentication 2023-06-25 00:40:59 -07:00
Bastien Faure 48c4928767 Implement default profile detection in GCP 2023-06-24 23:21:26 -07:00
Bastien Faure 7578d4299d Implemented a logger to be used by cloudfox providers, moved version to globals 2023-06-24 23:10:54 -07:00
Bastien Faure b08ca87e6b Continue profile management implementation for GCP 2023-06-24 21:15:56 -07:00
Bastien Faure 5f029319a6 Continue GCP implementation, including multiple profile selection on CLI 2023-06-24 20:34:13 -07:00
Bastien Faure 42a32bb8e3 Added hierarchy command, aiming to display a resource tree, started decomissioning whoami 2023-06-22 22:14:47 -07:00
Bastien Faure c397f858db Added an internal function to list 'usable' credentials 2023-06-22 17:29:14 -07:00
Bastien Faure 980e28bc9d Implemented access-tokens commands for GCP to enumerate local tokens in a detailed manner 2023-06-22 12:35:48 -07:00
David 9b752ce4e6 saving work 2023-06-22 10:31:05 -04:00
Bastien Faure c102ea8d17 Started working around local GCP credentials and added profiles and inventory command stubs 2023-06-22 00:07:48 -07:00
Bastien Faure ba04dade7a Mimick AWS structure for GCP CLI args and modules 2023-06-21 13:20:14 -07:00
Bastien Faure 61cb727a64 Remove GCP debug 2023-06-20 22:40:22 -07:00
Bastien Faure 25fb66d6a1 Moved GCP helpers to separate file 2023-06-20 22:39:35 -07:00
Bastien Faure 82adab9863 Updated README with GCP documentation 2023-06-20 13:25:28 -07:00
Bastien Faure 02026c7a78 Initial commit for GCP implementation 2023-06-20 13:17:40 -07:00
431 changed files with 6005 additions and 197483 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
* @bishopfaure @jbarciabf @msperling-bf
@sethsec-bf carlosvendramini-bf
+3 -3
View File
@@ -15,8 +15,8 @@ jobs:
steps:
- name: Go 1.21
uses: actions/setup-go@v4
- name: Go 1.20
uses: actions/setup-go@v2
with:
go-version: ^1.20
id: go
@@ -28,7 +28,7 @@ jobs:
gpg --list-secret-keys --keyid-format LONG
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
-16
View File
@@ -1,16 +0,0 @@
# This Action uses minimal steps to run in ~5 seconds to rapidly:
# Looks for typos in the codebase using codespell
# https://github.com/codespell-project/codespell#readme
name: codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
codespell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install --user codespell
- run: codespell --ignore-words-list="aks,referers,invokable" --skip="*.sum"
+1 -12
View File
@@ -15,7 +15,6 @@
# Dependency directories (remove the comment below to include it)
# vendor/
.vscode
.idea
launch.json
######################################################
@@ -62,22 +61,12 @@ terraform.rc
.DS_Store
untracked/*
*tmp*
tmp/*
output/*
*cloudfox-output*
cloudfox-*
cloudfox
cloudfox-macos
cloudfox-linux
*.log
*.bak
*.txt
*.json
*.csv
*.log
dist/
# graphvis files
*.gv
*.svg
*.log
+20 -40
View File
@@ -1,26 +1,20 @@
run:
go run main.go
/home/marauder/Downloads/go/bin/go run main.go
windows:
GOOS=windows GOARCH=386 go build
GOOS=windows GOARCH=386 /home/marauder/Downloads/go/bin/go build
sha1sum cloudfox.exe > sha1sum-win.txt
zip cloudfox-win.zip cloudfox.exe sha1sum-win.txt
rm cloudfox.exe sha1sum-win.txt
linux:
GOOS=linux GOARCH=386 go build
GOOS=linux GOARCH=386 /home/marauder/Downloads/go/bin/go build
sha1sum cloudfox > sha1sum-linux.txt
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
GOOS=darwin GOARCH=amd64 /home/marauder/Downloads/go/bin/go build
sha1sum cloudfox > sha1sum-mac.txt
zip cloudfox-mac.zip cloudfox sha1sum-mac.txt
rm cloudfox sha1sum-mac.txt
@@ -30,36 +24,22 @@ all: windows linux macos
.PHONY: release
release: clean
mkdir -p ./cloudfox
mkdir -p ./cloudfox
GOOS=windows GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox.exe .
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe
rm -rf ./cloudfox/cloudfox.exe
GOOS=linux GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox .
rm -rf ./cloudfox/cloudfox
GOOS=windows GOARCH=amd64 go build -o ./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=darwin GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox
rm -rf ./cloudfox/cloudfox
GOOS=darwin GOARCH=arm64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox
rm -rf ./cloudfox/cloudfox
GOOS=linux GOARCH=amd64 go build -o ./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 .
sha1sum ./cloudfox/cloudfox > ./cloudfox/sha1sum.txt
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
rm -rf ./cloudfox/cloudfox ./cloudfox/sha1sum.txt
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:
clean:
rm -rf ./cloudfox
+20 -201
View File
@@ -1,10 +1,6 @@
# :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?
@@ -18,17 +14,8 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments.
## Demos, Examples, Walkthroughs
* [Blog - Introducing: CloudFox](https://bishopfox.com/blog/introducing-cloudfox)
* [Video - CloudFox + CloudFoxable A Powerful Duo for Mastering the Art of Identifying and Exploiting AWS Attack Paths](https://www.youtube.com/watch?v=RdQiIvCrSzk)
* [Video - Penetration Testing with CloudFox](https://www.youtube.com/watch?v=Ljt_JUp5HbM)
* [Video - CloudFox Intro Demos](https://www.youtube.com/watch?v=ReWoUgpUuiQ)
* [Video - Tool Talk: CloudFox AWS sub-command walkthroughs](https://youtu.be/KKsYfL5uVU4?t=360)
## Intentionally Vulnerable Playground
* [CloudFoxable - A Gamified Cloud Hacking Sandbox](https://cloudfoxable.bishopfox.com/)
## Want to chat about CloudFox?
Join us on the [RedSec discord server](https://discord.gg/redsec)
* [Video - Tool Talk: CloudFox AWS sub-command walkthroughs](https://youtu.be/KKsYfL5uVU4?t=360)
## Quick Start
CloudFox is modular (you can run one command at a time), but there is an `aws all-checks` command that will run the other aws commands for you with sane defaults:
@@ -52,46 +39,32 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
| Provider| CloudFox Commands |
| - | - |
| AWS | 34 |
| Azure | 4 |
| GCP | 60 |
| Kubernetes | 23 |
| AWS | 30 |
| Azure | 4 |
| GCP | 4 |
| Kubernetes | Support Planned |
# Install
**Option 1:** Download the [latest binary release](https://github.com/BishopFox/cloudfox/releases) for your platform.
**Option 2:** If you use homebrew: `brew install cloudfox`
**Option 3:** [Install Go](https://golang.org/doc/install), use `go install github.com/BishopFox/cloudfox@latest` to install from the remote source
**Option 4:** Developer mode:
[Install Go](https://golang.org/doc/install), clone the CloudFox repository and compile from source
**Option 2:** [Install Go](https://golang.org/doc/install), clone the CloudFox repository and compile from source
```
# git clone https://github.com/BishopFox/cloudfox.git
...omitted for brevity...
# cd ./cloudfox
# Make any changes necessary
# go build .
# ./cloudfox
```
**Option 5:** Testing a bug fix
```
git clone [email protected]:BishopFox/cloudfox.git
git checkout seth-dev
go build .
./cloudfox [rest of the command options]
```
# Prerequisites
### AWS
* AWS CLI installed
* Supports AWS profiles, AWS environment variables, or metadata retrieval (on an ec2 instance)
* To run commands on multiple profiles at once, you can specify the path to a file with a list of profile names separated by a new line using the `-l` flag or pass all stored profiles with the `-a` flag.
* To run commands on multiple profiles at once, you can specify the path to a file with a list of profile names seperated by a new line using the `-l` flag or pass all stored profiles with the `-a` flag.
* A principal with one recommended policies attached (described below)
* Recommended attached policies: **`SecurityAudit` + [CloudFox custom policy](./misc/aws/cloudfox-policy.json)**
@@ -109,65 +82,17 @@ Additional policy notes (as of 09/2022):
* 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).
### Kubernetes
* `Get` and `List` on all (`*`) resources
* `gcloud` CLI installed
* Proper SDK credentials available (`gcloud auth application-default login`)
* Viewer or similar permissions applied.
# AWS Commands
| Provider | Command Name | Description
| - | - | - |
| AWS | [all-checks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#all-checks) | Run all of the other commands using reasonable defaults. You'll still want to check out the non-default options of each command, but this is a great place to start. |
| 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. |
| AWS | [ecr](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ecr) | List the most recently pushed image URI from all repositories. Use the loot file to pull selected images down with docker/nerdctl for inspection. |
| AWS | [ecs-tasks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ecs-tasks) | List all ecs tasks. This returns a list of ecs tasks and associated cluster, task definition, container instance, launch type, and associated IAM principal. |
| AWS | [eks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#eks) | List all EKS clusters, see if they expose their endpoint publicly, and check the associated IAM roles attached to reach cluster or node group. Generates a loot file with the `aws eks udpate-kubeconfig` command needed to connect to each cluster. |
@@ -180,145 +105,39 @@ For detailed setup instructions, see the [GCP Setup Guide](https://github.com/Bi
| AWS | [inventory](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#inventory) | Gain a rough understanding of size of the account and preferred regions. |
| AWS | [lambda](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#lambda) | Lists the lambda functions in the account, including which one's have admin roles attached. Also gives you handy commands for downloading each function. |
| AWS | [network-ports](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#network-ports) | Enumerates AWS services that are potentially exposing a network service. The security groups and the network ACLs are parsed for each resource to determine what ports are potentially exposed. |
| AWS | [orgs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#orgs) | Enumerate accounts in an organization |
| AWS | [outbound-assumed-roles](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
| AWS | [outbound-assumed-roles](#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
| AWS | [permissions](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#permissions) | Enumerates IAM permissions associated with all users and roles. Grep this output to figure out what permissions a particular principal has rather than logging into the AWS console and painstakingly expanding each policy attached to the principal you are investigating. |
| 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 | [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. 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`. |
| AWS | [sns](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sns) | This command enumerates all of the sns topics and gives you the commands to subscribe to a topic or send messages to a topic (if you have the permissions needed). This command only deals with topics, and not the SMS functionality. This command also attempts to summarize topic resource policies if they exist.|
| 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
| Provider | Command Name | Description
| - | - | - |
| Azure | [whoami](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#whoami) | Displays information on the tenant, subscriptions and resource groups available to your current Azure CLI session. This is useful to provide situation awareness on what tenant and subscription IDs to use with the other sub commands. |
| Azure | [inventory](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#inventory) | Display an inventory table of all resources per location. |
| Azure | [instances](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#instances) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
| Azure | [rbac](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac) | Lists Azure RBAC role assignments at subscription or tenant level |
| Azure | [storage](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#storage) | The storage command is still under development. Currently it only displays limited data about the storage accounts |
| Azure | [vms](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#vms) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
# 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 |
# Kubernetes Commands
`cloudfox kubernetes --help`
or
`cloudfox k8s --help`
| Provider | Command Name | Description
| - | - | - |
| k8s | [whoami](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#whoami) | Display the identity the kubeconfig, ServiceAccount token, or in-cluster credentials are authenticated as |
| k8s | [all-checks](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#all-checks) | Executes all available Kubernetes commands to collect and display information from the cluster |
| k8s | [configmaps](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#configmaps) | List all cluster ConfigMaps and detailed information |
| k8s | [cronjobs](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#cronjobs) | List all cluster CronJobs |
| k8s | [daemonsets](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#daemonsets) | List all cluster DaemonSets |
| k8s | [deployments](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#deployments) | List all cluster Deployments |
| k8s | [endpoints](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#endpoints) | List all cluster Endpoints |
| k8s | [hidden-admins](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#hidden-admins) | Enumerate roles, users, groups, and service accounts with dangerous or escalatory permissions |
| k8s | [jobs](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#jobs) | List all cluster Jobs |
| k8s | [namespaces](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#namespaces) | List all cluster Namespaces
| k8s | [network-exposure](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#network-exposure) | Enumerate exposed network hosts and ports
| k8s | [network-policy](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#network-policy) | List all cluster Network Policies
| k8s | [nodes](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#nodes) | List all cluster Nodes
| k8s | [permissions](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#permissions) | List all cluster entities and their respective permissions
| k8s | [pod-security](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#pod-security) | List all cluster Pod Security Policies or Admission settings
| k8s | [pods](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#pods) | List all cluster Pods
| k8s | [replicasets](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#replicasets) | List all cluster ReplicaSets
| k8s | [secrets](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#secrets) | List all cluster Secrets
| k8s | [services](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#services) | List all cluster Services
| k8s | [statefulsets](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#statefulsets) | List all cluster StatefulSets
| k8s | [taints-tolerations](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#taints-tolerations) | Displays for each pod which node taints it tolerates and lists taints on nodes that are not tolerated by any pods
| k8s | [taints](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#taints) | List all cluster node taints and outputs an example YAML file with node and tolerations to create a pod
| k8s | [tolerations](https://github.com/BishopFox/cloudfox/wiki/k8s-Commands#tolerations) | List all cluster pod tolerations and outputs an example YAML file with tolerations to create a pod
| - | - | - |
| GCP | [access-tokens](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#access-tokens) | Displays handy information about existing GCP credentials on your local machine |
| GCP | [hierarchy](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#hierarchy) Displays information on the projects available to your current Gcloud SDK credentials. This is useful to provide situation awareness on what organizations, folders and projects to use with the other sub commands.|
| GCP | [inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#inventory) Gain a rough understanding of size of the available organizations |
| GCP | [buckets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#buckets) Lists the buckets in the available organizations and gives you handy commands for inspecting them further. |
# 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)
@@ -332,7 +151,7 @@ CloudFox doesn't create any alerts or findings, and doesn't check your environme
**Why do I see errors in some CloudFox commands?**
* Services that don't exist in all regions - CloudFox tries a few ways to figure out what services are supported in each region. However some services don't support the methods CloudFox uses, so CloudFox defaults to just asking every region about the service. Regions that don't support the service will return errors.
* Services that don't exist in all regions - CloudFox tries a few ways to figure out what services are supported in each region. However some services don't support the methods CloudFox uses, so CloudFox defaults to just asking every region about the service. Regions that don't suppor the service will return errors.
* You don't have permission - Another reason you might see errors if you don't have permissions to make calls that CloudFox is making. Either because the policy doesn't allow it (e.g., SecurityAudit doesn't allow all of the permissions CloudFox needs. Or, it might be an SCP that is blocking you.
You can always look in the ~/.cloudfox/cloudfox-error.log file to get more information on errors.
+9 -37
View File
@@ -5,7 +5,6 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
@@ -19,10 +18,9 @@ type AccessKeysModule struct {
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSProfile string
OutputFormat string
Goroutines int
WrapTable bool
AWSOutputType string
AWSTableCols string
CommandCounter internal.CommandCounter
// Main module data
@@ -38,8 +36,8 @@ type UserKeys struct {
Key string
}
func (m *AccessKeysModule) PrintAccessKeys(filter string, outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "access-keys"
@@ -57,7 +55,6 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputDirectory string
// Variables used to draw table output
m.output.Headers = []string{
"Account",
"User Name",
"Access Key ID",
}
@@ -68,7 +65,6 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputDirectory string
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
key.Username,
key.Key,
},
@@ -83,6 +79,9 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputDirectory string
//fmt.Printf("[%s][%s] Preparing output.\n\n")
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -90,37 +89,10 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputDirectory string
Wrap: m.WrapTable,
},
}
// 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 != "" {
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"User Name",
"Access Key ID",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"User Name",
"Access Key ID",
}
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
-686
View File
@@ -1,686 +0,0 @@
package aws
import (
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
apigatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
apigatewayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
var CURL_COMMAND string = "curl -X %s %s"
type ApiGwModule struct {
// General configuration data
APIGatewayClient sdk.APIGatewayClientInterface
APIGatewayv2Client sdk.APIGatewayv2ClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
// Main module data
Gateways []ApiGateway
CommandCounter internal.CommandCounter
Errors []string
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type ApiGateway struct {
AWSService string
Region string
Name string
Endpoint string
ApiKey string
Public string
Method string
}
func (m *ApiGwModule) PrintApiGws(outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "api-gw"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s][%s] Enumerating api-gateways for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), 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 ApiGateway)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
//execute regional checks
for _, region := range m.AWSRegions {
wg.Add(1)
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
receiverDone <- true
<-receiverDone
sort.Slice(m.Gateways, func(i, j int) bool {
return m.Gateways[i].AWSService < m.Gateways[j].AWSService
})
m.output.Headers = []string{
"Service",
"Region",
"Name",
"Method",
"Endpoint",
"ApiKey",
"Public",
}
// Table rows
for i := range m.Gateways {
m.output.Body = append(
m.output.Body,
[]string{
m.Gateways[i].AWSService,
m.Gateways[i].Region,
m.Gateways[i].Name,
m.Gateways[i].Method,
m.Gateways[i].Endpoint,
m.Gateways[i].ApiKey,
m.Gateways[i].Public,
},
)
}
if len(m.output.Body) > 0 {
filepath := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
DirectoryName: filepath,
},
Loot: internal.LootClient{
DirectoryName: filepath,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
loot := m.writeLoot(filepath, verbosity)
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
Name: m.output.CallingModule,
Contents: loot,
})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
fmt.Printf("[%s][%s] %s API gateways found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No API gateways found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
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 *ApiGwModule) Receiver(receiver chan ApiGateway, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Gateways = append(m.Gateways, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *ApiGwModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer wg.Done()
// check the concurrency semaphore
// semaphore <- struct{}{}
// defer func() {
// <-semaphore
// }()
// 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("api-gateway", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getAPIGatewayAPIsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
wg.Add(1)
go m.getAPIGatewayVIPsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
wg.Add(1)
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
wg.Add(1)
go m.getAPIGatewayv2VIPsPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) string {
path := filepath.Join(outputDirectory, "loot")
f := filepath.Join(path, "api-gws.txt")
var out string
for _, endpoint := range m.Gateways {
method := endpoint.Method
// Write a GET and POST for ANY
if endpoint.Method == "ANY" {
line := fmt.Sprintf(CURL_COMMAND, "GET", endpoint.Endpoint)
if endpoint.ApiKey != "" {
line += fmt.Sprintf(" -H 'X-Api-Key: %s'", endpoint.ApiKey)
}
out += line + "\n"
method = "POST"
}
line := fmt.Sprintf(CURL_COMMAND, method, endpoint.Endpoint)
if endpoint.ApiKey != "" {
line += fmt.Sprintf(" -H 'X-Api-Key: %s'", endpoint.ApiKey)
}
if method == "DELETE" || method == "PATCH" || method == "POST" || method == "PUT" {
line += " -H 'Content-Type: application/json' -d '{}'"
}
out += line + "\n"
}
// err = os.WriteFile(f, []byte(out), 0644)
// if err != nil {
// m.modLog.Error(err.Error())
// m.CommandCounter.Error++
// panic(err.Error())
// }
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Send these requests through your favorite interception proxy"))
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] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
return out
}
func (m *ApiGwModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, api := range Items {
m.CommandCounter.Total++
for _, endpoint := range m.getEndpointsPerAPIGateway(r, api) {
dataReceiver <- endpoint
}
}
}
func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
GetDomainNames, err := sdk.CachedApiGatewayGetDomainNames(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, item := range GetDomainNames {
domain := aws.ToString(item.DomainName)
GetBasePathMappings, err := sdk.CachedApiGatewayGetBasePathMappings(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, item.DomainName)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, mapping := range GetBasePathMappings {
stage := aws.ToString(mapping.Stage)
basePath := aws.ToString(mapping.BasePath)
if basePath == "(none)" {
basePath = "" // Empty string since '/' is already prepended
}
for _, api := range Items {
if api.Id != nil && aws.ToString(api.Id) == aws.ToString(mapping.RestApiId) {
m.CommandCounter.Total++
endpoints := m.getEndpointsPerAPIGateway(r, api)
for _, endpoint := range endpoints {
old := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.RestApiId), r, stage)
if strings.HasPrefix(endpoint.Endpoint, old) {
var new string
if basePath == "" {
new = fmt.Sprintf("https://%s/", domain)
} else {
new = fmt.Sprintf("https://%s/%s/", domain, basePath)
}
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
}
}
break
}
}
}
}
}
func (m *ApiGwModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.RestApi) []ApiGateway {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
}()
var gateways []ApiGateway
//var PaginationControl2 *string
awsService := "APIGateway"
var public string
name := aws.ToString(api.Name)
id := aws.ToString(api.Id)
raw_endpoint := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com", id, r)
endpointType := *api.EndpointConfiguration
//fmt.Println(endpointType)
if endpointType.Types[0] == "PRIVATE" {
public = "False"
} else {
public = "True"
}
GetStages, err := sdk.CachedApiGatewayGetStages(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return gateways
}
resources, err := sdk.CachedApiGatewayGetResources(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, stage := range GetStages.Item {
stageName := aws.ToString(stage.StageName)
for _, resource := range resources {
if len(resource.ResourceMethods) != 0 {
for method := range resource.ResourceMethods {
// Check if API Key is required for endpoint
apiKey := ""
if m.ApiGatewayApiKeyRequired(r, api.Id, resource.Id, method) {
apiKey, err = m.GetApiGatewayApiKey(r, id, stageName)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
}
path := aws.ToString(resource.Path)
endpoint := fmt.Sprintf("%s/%s%s", raw_endpoint, stageName, path)
gateways = append(gateways, ApiGateway{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Method: method,
Public: public,
ApiKey: apiKey,
})
}
}
}
}
return gateways
}
func (m *ApiGwModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, api := range Items {
m.CommandCounter.Total++
for _, endpoint := range m.getEndpointsPerAPIGatewayv2(r, api) {
dataReceiver <- endpoint
}
}
}
func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
GetDomainNames, err := sdk.CachedAPIGatewayv2GetDomainNames(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, item := range GetDomainNames {
domain := aws.ToString(item.DomainName)
GetApiMappings, err := sdk.CachedAPIGatewayv2GetApiMappings(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, domain)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, mapping := range GetApiMappings {
stage := aws.ToString(mapping.Stage)
if stage == "$default" {
stage = ""
}
path := aws.ToString(mapping.ApiMappingKey)
for _, api := range Items {
if api.ApiId != nil && aws.ToString(api.ApiId) == aws.ToString(mapping.ApiId) {
m.CommandCounter.Total++
endpoints := m.getEndpointsPerAPIGatewayv2(r, api)
for _, endpoint := range endpoints {
var old string
if stage == "" {
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/", aws.ToString(mapping.ApiId), r)
} else {
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.ApiId), r, stage)
}
if strings.HasPrefix(endpoint.Endpoint, old) {
var new string
if path == "" {
new = fmt.Sprintf("https://%s/", domain)
} else {
new = fmt.Sprintf("https://%s/%s/", domain, path)
}
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
}
}
break
}
}
}
}
}
func (m *ApiGwModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2Types.Api) []ApiGateway {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
}()
var gateways []ApiGateway
awsService := "APIGatewayv2"
var public string
name := aws.ToString(api.Name)
raw_endpoint := aws.ToString(api.ApiEndpoint)
id := aws.ToString(api.ApiId)
var stages []string
GetStages, err := sdk.CachedAPIGatewayv2GetStages(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, id)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, stage := range GetStages {
s := aws.ToString(stage.StageName)
if s == "$default" {
s = ""
}
stages = append(stages, s)
}
GetRoutes, err := sdk.CachedAPIGatewayv2GetRoutes(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, id)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, stage := range stages {
for _, route := range GetRoutes {
routeKey := route.RouteKey
var method string
var path string
if len(strings.Fields(*routeKey)) == 2 {
method = strings.Fields(*routeKey)[0]
path = strings.Fields(*routeKey)[1]
}
var endpoint string
if stage == "" {
endpoint = fmt.Sprintf("%s%s", raw_endpoint, path)
} else {
endpoint = fmt.Sprintf("%s/%s%s", raw_endpoint, stage, path)
}
public = "True"
gateways = append(gateways, ApiGateway{
AWSService: awsService,
Region: r,
Name: name,
Method: method,
Endpoint: endpoint,
Public: public,
})
}
}
return gateways
}
func (m *ApiGwModule) ApiGatewayApiKeyRequired(r string, ApiId *string, ResourceId *string, method string) bool {
GetMethod, err := sdk.CachedApiGatewayGetMethod(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, aws.ToString(ApiId), aws.ToString(ResourceId), method)
if err != nil {
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
m.modLog.Error(err.Error())
m.CommandCounter.Error++
} else {
return aws.ToBool(GetMethod.ApiKeyRequired)
}
return false
}
func (m *ApiGwModule) GetApiGatewayApiKey(r string, ApiId string, Stage string) (string, error) {
var items []apigatewayTypes.UsagePlan
GetUsagePlans, err := sdk.CachedApiGatewayGetUsagePlans(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
if err != nil {
return "", err
}
for _, item := range GetUsagePlans {
items = append(items, item)
}
for _, item := range items {
for _, apiStage := range item.ApiStages {
if aws.ToString(apiStage.ApiId) == ApiId && aws.ToString(apiStage.Stage) == Stage {
GetUsagePlanKeys, err := sdk.CachedApiGatewayGetUsagePlanKeys(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, aws.ToString(item.Id))
if err != nil {
return "", err
}
for _, i := range GetUsagePlanKeys {
if aws.ToString(i.Type) == "API_KEY" {
return aws.ToString(i.Value), nil
}
}
}
}
}
return "", nil
}
-67
View File
@@ -1,67 +0,0 @@
package aws
import (
"path/filepath"
"strings"
"testing"
"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"
"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"},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
Account: aws.String("123456789012"),
},
Goroutines: 3,
WrapTable: false,
APIGatewayClient: &sdk.MockedAWSAPIGatewayClient{},
APIGatewayv2Client: &sdk.MockedAWSAPIGatewayv2Client{},
ServiceMap: serviceMap,
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
//tmpDir := "~/.cloudfox/"
tmpDir := ptr.ToString(internal.GetLogDirPath())
m.PrintApiGws(tmpDir, 2)
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/api-gw.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
}
//print the results file to the screen
//fmt.Println(string(resultsFile))
// I want a test that runs the main function and checks the output to see if the following items are in the output: "https://qwerty.execute-api.us-east-1.amazonaws.com/stage1/path1", "https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage1/route2"
expectedResults := []string{
"https://qwerty.execute-api.us-east-1.amazonaws.com/stage1/path1",
"https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage1/route2",
"https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage2/route1",
"23oieuwefo3rfs",
}
for _, expected := range expectedResults {
if !strings.Contains(string(resultsFile), expected) {
t.Errorf("Expected %s to be in the results file", expected)
}
}
}
+47 -93
View File
@@ -20,26 +20,24 @@ import (
type BucketsModule struct {
// General configuration data
//BucketsS3Client CloudFoxS3Client
CheckBucketPolicies bool
S3Client sdk.AWSS3ClientInterface
AWSRegions []string
AWSProfile string
Caller sts.GetCallerIdentityOutput
AWSTableCols string
AWSOutputType string
S3Client sdk.AWSS3ClientInterface
AWSRegions []string
AWSProfile string
Caller sts.GetCallerIdentityOutput
Goroutines int
WrapTable bool
OutputFormat string
Goroutines int
WrapTable bool
// Main module data
Buckets []BucketRow
Buckets []Bucket
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type BucketRow struct {
type Bucket struct {
Arn string
AWSService string
Region string
@@ -55,8 +53,8 @@ type BucketRow struct {
ResourcePolicySummary string
}
func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "buckets"
@@ -80,7 +78,7 @@ func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan BucketRow)
dataReceiver := make(chan Bucket)
// Create a channel to signal to stop
receiverDone := make(chan bool)
@@ -102,7 +100,6 @@ func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Name",
"Region",
"Public?",
@@ -114,16 +111,17 @@ func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Buckets[i].Name,
m.Buckets[i].Region,
m.Buckets[i].IsPublic,
m.Buckets[i].ResourcePolicySummary,
},
)
}
}
if len(m.output.Body) > 0 {
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -131,47 +129,10 @@ func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
Wrap: m.WrapTable,
},
}
// 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 != "" {
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Name",
"Region",
"Public?",
"Resource Policy Summary",
}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{
"Name",
"Region",
"Public?",
"Resource Policy Summary",
}
}
// Remove the Public? and Resource Policy Summary columns if the user did not specify CheckBucketPolicies
if !m.CheckBucketPolicies {
tableCols = removeStringFromSlice(tableCols, "Public?")
tableCols = removeStringFromSlice(tableCols, "Resource Policy Summary")
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
TableCols: tableCols,
Body: m.output.Body,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -187,7 +148,7 @@ func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
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 *BucketsModule) Receiver(receiver chan BucketRow, receiverDone chan bool) {
func (m *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
@@ -200,7 +161,7 @@ func (m *BucketsModule) Receiver(receiver chan BucketRow, receiverDone chan bool
}
}
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) {
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) {
defer wg.Done()
m.CommandCounter.Total++
@@ -254,7 +215,7 @@ func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile
}
func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) {
func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
@@ -276,49 +237,42 @@ func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, sem
}
for _, b := range ListBuckets {
bucket := &BucketRow{
bucket := &Bucket{
Name: aws.ToString(b.Name),
AWSService: "S3",
}
region, err = sdk.CachedGetBucketLocation(m.S3Client, aws.ToString(m.Caller.Account), aws.ToString(b.Name))
if err != nil {
m.modLog.Error(err.Error())
region = "Unknown"
}
bucket.Region = region
if m.CheckBucketPolicies {
policyJSON, err := sdk.CachedGetBucketPolicy(m.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
if err != nil {
m.modLog.Error(err.Error())
} else {
bucket.PolicyJSON = policyJSON
}
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
} else {
bucket.Policy = policy
}
bucket.IsPublic = "No"
if !bucket.Policy.IsEmpty() {
m.analyseBucketPolicy(bucket, dataReceiver)
} else {
bucket.Access = "No resource policy"
dataReceiver <- *bucket
}
policyJSON, err := sdk.CachedGetBucketPolicy(m.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
if err != nil {
m.modLog.Error(err.Error())
} else {
bucket.PolicyJSON = policyJSON
}
// Send Bucket object through the channel to the receiver
bucket.Access = "Skipped"
bucket.IsPublic = "Skipped"
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
} else {
bucket.Policy = policy
}
bucket.IsPublic = "No"
if !bucket.Policy.IsEmpty() {
m.analyseBucketPolicy(bucket, dataReceiver)
} else {
bucket.Access = "No resource policy"
dataReceiver <- *bucket
}
// Send Bucket object through the channel to the receiver
}
}
func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool {
@@ -326,11 +280,11 @@ func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool
if err != nil {
return false
}
return aws.ToBool(publicAccessBlock.IgnorePublicAcls) && aws.ToBool(publicAccessBlock.BlockPublicPolicy) && aws.ToBool(publicAccessBlock.RestrictPublicBuckets)
return publicAccessBlock.IgnorePublicAcls
}
func (m *BucketsModule) analyseBucketPolicy(bucket *BucketRow, dataReceiver chan BucketRow) {
func (m *BucketsModule) analyseBucketPolicy(bucket *Bucket, dataReceiver chan Bucket) {
m.storeAccessPolicy(bucket)
if bucket.Policy.IsPublic() && !bucket.Policy.IsConditionallyPublic() && !m.isPublicAccessBlocked(bucket.Name, bucket.Region) {
@@ -345,14 +299,14 @@ func (m *BucketsModule) analyseBucketPolicy(bucket *BucketRow, dataReceiver chan
} else {
bucket.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
//bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n")
bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n")
}
dataReceiver <- *bucket
}
func (m *BucketsModule) storeAccessPolicy(bucket *BucketRow) {
func (m *BucketsModule) storeAccessPolicy(bucket *Bucket) {
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", bucket.Name))
if err := m.storeFile(f, bucket.PolicyJSON); err != nil {
+11 -13
View File
@@ -63,10 +63,10 @@ func (m *MockedS3Client) GetBucketLocation(ctx context.Context, params *s3.GetBu
func (m *MockedS3Client) GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) {
output := &s3.GetPublicAccessBlockOutput{
PublicAccessBlockConfiguration: &types.PublicAccessBlockConfiguration{
BlockPublicAcls: aws.Bool(true),
BlockPublicPolicy: aws.Bool(true),
IgnorePublicAcls: aws.Bool(false),
RestrictPublicBuckets: aws.Bool(true),
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: false,
RestrictPublicBuckets: true,
},
}
return output, nil
@@ -81,11 +81,9 @@ func TestListBuckets(t *testing.T) {
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
Account: aws.String("123456789012"),
},
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
AWSProfile: "unittesting",
AWSOutputType: "",
CheckBucketPolicies: true,
Goroutines: 3,
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
AWSProfile: "unittesting",
Goroutines: 3,
}
subtests := []struct {
@@ -93,14 +91,14 @@ func TestListBuckets(t *testing.T) {
outputDirectory string
verbosity int
testModule BucketsModule
expectedResult []BucketRow
expectedResult []Bucket
}{
{
name: "test1",
outputDirectory: ".",
verbosity: 2,
testModule: m,
expectedResult: []BucketRow{{
expectedResult: []Bucket{{
Name: "mockBucket123",
}},
},
@@ -112,9 +110,9 @@ func TestListBuckets(t *testing.T) {
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintBuckets(subtest.outputDirectory, subtest.verbosity)
subtest.testModule.PrintBuckets(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedBucket := range subtest.expectedResult {
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/buckets.txt")
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/buckets.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
-538
View File
@@ -1,538 +0,0 @@
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
View File
File diff suppressed because it is too large. Load diff
+29 -64
View File
@@ -3,44 +3,38 @@ 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"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
func InitIamCommandClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
func initIAMSimClient(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"}),
IAMClient: iamSimPPClient,
Caller: caller,
AWSProfile: AWSProfile,
Goroutines: Goroutines,
}
return iamSimMod
}
func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSWrapTable bool, AWSMFAToken string) SNSModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSWrapTable bool) SNSModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
cloudFoxSNSClient := SNSModule{
SNSClient: sns.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
}
@@ -48,25 +42,25 @@ func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string
}
func initCloudFoxS3Client(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, AWSMFAToken string) BucketsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
func initCloudFoxS3Client(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string) BucketsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
cloudFoxS3Client := BucketsModule{
S3Client: s3.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return cloudFoxS3Client
}
func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) SQSModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) SQSModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
sqsClient := SQSModule{
SQSClient: sqs.NewFromConfig(AWSConfig),
Caller: caller,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
AWSProfile: AWSProfile,
Goroutines: Goroutines,
}
@@ -75,87 +69,58 @@ func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVers
}
func InitLambdaClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) LambdasModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
func InitLambdaClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) LambdasModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
lambdaClient := LambdasModule{
LambdaClient: lambda.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return lambdaClient
}
func InitCodeBuildClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) CodeBuildModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
func InitCodeBuildClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) CodeBuildModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
codeBuildClient := CodeBuildModule{
CodeBuildClient: codebuild.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return codeBuildClient
}
func InitECRClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) ECRModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
func InitECRClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) ECRModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
ecrClient := ECRModule{
ECRClient: ecr.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return ecrClient
}
func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) FilesystemsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) FilesystemsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
fileSystemsClient := FilesystemsModule{
EFSClient: efs.NewFromConfig(AWSConfig),
FSxClient: fsx.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return fileSystemsClient
}
func InitOrgsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) OrgModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) OrgModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
orgClient := OrgModule{
OrganizationsClient: organizations.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
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)
}
func InitGlueClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) *glue.Client {
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)
}
+34 -71
View File
@@ -5,7 +5,6 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
@@ -21,14 +20,12 @@ type CloudformationModule struct {
// General configuration data
CloudFormationClient sdk.CloudFormationClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
AWSProfile string
WrapTable bool
AWSOutputType string
AWSTableCols string
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
CFStacks []CFStack
@@ -48,8 +45,8 @@ type CFStack struct {
Template string
}
func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "cloudformation"
@@ -96,45 +93,48 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string,
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Name",
"Role",
"Parameters",
"Outputs",
// "Parameters",
// "Outputs",
}
// Table rows
for i := range m.CFStacks {
var hasParameters string
var hasOutputs string
if m.CFStacks[i].Parameters != nil {
hasParameters = "Y"
} else {
hasParameters = "N"
}
if m.CFStacks[i].Outputs != nil {
hasOutputs = "Y"
} else {
hasOutputs = "N"
}
// var isParameters string
// var isOutputs string
// if m.CFStacks[i].Parameters != nil {
// isParameters = "Y"
// } else {
// isParameters = "N"
// }
// if m.CFStacks[i].Outputs != nil {
// isOutputs = "Y"
// } else {
// isOutputs = "N"
// }
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.CFStacks[i].AWSService,
m.CFStacks[i].Region,
m.CFStacks[i].Name,
m.CFStacks[i].Role,
hasParameters,
hasOutputs,
// isParameters,
// isOutputs,
},
)
}
if len(m.output.Body) > 0 {
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -142,43 +142,10 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string,
Wrap: m.WrapTable,
},
}
// 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 != "" {
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Service",
"Region",
"Name",
"Role",
"Parameters",
"Outputs",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"Role",
}
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -193,12 +160,8 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string,
func (m *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
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",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
serviceRegions, err := servicemap.GetRegionsForService("cloudformation")
if err != nil {
+70 -3
View File
@@ -1,15 +1,81 @@
package aws
import (
"context"
"encoding/json"
"log"
"testing"
"time"
"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/cloudformation"
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
const DESCRIBE_STACKS_TEST_FILE = "./test-data/cloudformation-describestacks.json"
const TEMPLATE_BODY_TEST_FILE = "./test-data/cloudformation-getTemplate.json"
type Stacks struct {
Stacks []struct {
StackID string `json:"StackId"`
Description string `json:"Description"`
Tags []interface{} `json:"Tags"`
Outputs []types.Output `json:"Outputs"`
Parameters []types.Parameter `json:"Parameters"`
StackStatusReason interface{} `json:"StackStatusReason"`
CreationTime time.Time `json:"CreationTime"`
Capabilities []interface{} `json:"Capabilities"`
StackName string `json:"StackName"`
RoleArn string `json:"RoleArn"`
StackStatus string `json:"StackStatus"`
DisableRollback bool `json:"DisableRollback"`
} `json:"Stacks"`
}
type TemplateBody struct {
TemplateBody string `json:"TemplateBody"`
}
type MockedCloudformationClient struct {
describeStacks Stacks
getTemplateBody TemplateBody
}
func (m *MockedCloudformationClient) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.describeStacks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_STACKS_TEST_FILE)
}
var stacks []types.Stack
for _, stack := range m.describeStacks.Stacks {
stacks = append(stacks, types.Stack{
StackName: &stack.StackName,
RoleARN: &stack.RoleArn,
Outputs: stack.Outputs,
Parameters: stack.Parameters,
})
}
return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil
}
func (m *MockedCloudformationClient) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.getTemplateBody)
if err != nil {
log.Fatalf("can't unmarshall file %s", TEMPLATE_BODY_TEST_FILE)
}
return &cloudformation.GetTemplateOutput{TemplateBody: &m.getTemplateBody.TemplateBody}, nil
}
func (m *MockedCloudformationClient) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) {
return &cloudformation.ListStacksOutput{}, nil
}
func TestCloudFormation(t *testing.T) {
subtests := []struct {
name string
@@ -23,8 +89,9 @@ func TestCloudFormation(t *testing.T) {
outputDirectory: ".",
verbosity: 2,
testModule: CloudformationModule{
CloudFormationClient: &sdk.MockedCloudformationClient{},
CloudFormationClient: &MockedCloudformationClient{},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
@@ -38,7 +105,7 @@ func TestCloudFormation(t *testing.T) {
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintCloudformationStacks(subtest.outputDirectory, subtest.verbosity)
subtest.testModule.PrintCloudformationStacks(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedStack := range subtest.expectedResult {
if expectedStack.Name != subtest.testModule.CFStacks[index].Name {
log.Fatal("Stack name does not match expected name")
+44 -65
View File
@@ -3,7 +3,6 @@ package aws
import (
"fmt"
"path/filepath"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
@@ -19,17 +18,13 @@ type CodeBuildModule struct {
CodeBuildClient sdk.CodeBuildClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat 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
@@ -51,8 +46,8 @@ type Project struct {
CanPrivEsc string
}
func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *CodeBuildModule) PrintCodeBuildProjects(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "codebuild"
@@ -66,8 +61,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.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
@@ -112,66 +107,50 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi
}
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Region",
"Name",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// 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",
if m.pmapperError == nil {
m.output.Headers = []string{
"Region",
"Name",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
m.output.Headers = []string{
"Region",
"Name",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
//"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.Projects {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Projects[i].Region,
m.Projects[i].Name,
m.Projects[i].Role,
m.Projects[i].Admin,
m.Projects[i].CanPrivEsc,
},
)
if m.pmapperError == nil {
m.output.Body = append(
m.output.Body,
[]string{
m.Projects[i].Region,
m.Projects[i].Name,
m.Projects[i].Role,
m.Projects[i].Admin,
m.Projects[i].CanPrivEsc,
},
)
} else {
m.output.Body = append(
m.output.Body,
[]string{
m.Projects[i].Region,
m.Projects[i].Name,
m.Projects[i].Role,
m.Projects[i].Admin,
},
)
}
}
var seen []string
@@ -183,6 +162,11 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
//fmt.Printf("[%s][%s] %d projects with a total of %d node groups found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(seen), len(m.output.Body))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -191,10 +175,9 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -212,12 +195,8 @@ 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("codebuild", r)
if err != nil {
+2 -2
View File
@@ -124,9 +124,9 @@ func TestCodeBuildProjects(t *testing.T) {
tmpDir := "."
// execute the module with verbosity set to 2
m.PrintCodeBuildProjects(tmpDir, 2)
m.PrintCodeBuildProjects("table", tmpDir, 2)
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/codebuild.txt")
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/codebuild.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
+145 -277
View File
@@ -3,10 +3,10 @@ package aws
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
@@ -19,18 +19,16 @@ import (
type DatabasesModule struct {
RDSClient sdk.RDSClientInterface
RedshiftClient sdk.AWSRedShiftClientInterface
RedshiftClient sdk.RedShiftClientInterface
DynamoDBClient sdk.DynamoDBClientInterface
DocDBClient sdk.DocDBClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Databases []Database
CommandCounter internal.CommandCounter
@@ -52,11 +50,10 @@ type Database struct {
Protocol string
Public string
Size string
Roles string
}
func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *DatabasesModule) PrintDatabases(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "databases"
@@ -68,13 +65,13 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
}
fmt.Printf("[%s][%s] Enumerating databases for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: RDS, Redshift, DynamoDB, DocumentDB, Neptune\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
fmt.Printf("[%s][%s] Supported Services: RDS, DynamoDB\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
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 task status spinner/updated
//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
@@ -104,7 +101,6 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
})
m.output.Headers = []string{
"Account",
"Service",
"Engine",
"Region",
@@ -112,57 +108,17 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
"Size",
"UserName",
"Endpoint",
"Port",
"Roles",
//"Port",
//"Protocol",
//"Public",
}
// 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",
"Service",
"Engine",
"Region",
"Name",
"Size",
"UserName",
"Endpoint",
"Port",
"Roles",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Engine",
"Region",
"Name",
"Size",
"UserName",
"Endpoint",
"Port",
}
}
// Table rows
for i := range m.Databases {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Databases[i].AWSService,
m.Databases[i].Engine,
m.Databases[i].Region,
@@ -170,8 +126,7 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
m.Databases[i].Size,
m.Databases[i].UserName,
m.Databases[i].Endpoint,
strconv.Itoa(int(m.Databases[i].Port)),
m.Databases[i].Roles,
// strconv.Itoa(int(m.Databases[i].Port)),
// m.Databases[i].Protocol,
// m.Databases[i].Public,
},
@@ -179,35 +134,27 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
}
if len(m.output.Body) > 0 {
filepath := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
DirectoryName: filepath,
},
Loot: internal.LootClient{
DirectoryName: filepath,
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,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
loot := m.writeLoot(filepath, verbosity)
//o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.writeLoot(o.Table.DirectoryName, verbosity)
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
Name: "databases-UrlsOnly.txt",
Contents: loot,
})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
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)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s databases found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No databases found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -232,87 +179,56 @@ 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
serviceMap := m.ServiceMap
if serviceMap == nil {
serviceMap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
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)
m.executeDynamoDbCheck(r, wg, semaphore, dataReceiver, serviceMap)
}
type check struct {
region string
wg *sync.WaitGroup
semaphore chan struct{}
dataReceiver chan Database
serviceMap *awsservicemap.AwsServiceMap
service string
executor func(string, *sync.WaitGroup, chan struct{}, chan Database)
}
func (m *DatabasesModule) executeCheck(check check) {
res, err := check.serviceMap.IsServiceInRegion(check.service, check.region)
res, err := servicemap.IsServiceInRegion("rds", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
check.wg.Add(1)
go check.executor(check.region, check.wg, check.semaphore, check.dataReceiver)
wg.Add(1)
go m.getRdsClustersPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("redshift", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getRedshiftDatabasesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("dynamodb", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getDynamoDBTablesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("docdb", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getDocDBTablesPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *DatabasesModule) executeRdsCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
m.executeCheck(check{
region: r,
wg: wg,
semaphore: semaphore,
dataReceiver: dataReceiver,
serviceMap: servicemap,
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) {
m.executeCheck(check{
region: r,
wg: wg,
semaphore: semaphore,
dataReceiver: dataReceiver,
serviceMap: servicemap,
service: "redshift",
executor: m.getRedshiftDatabasesPerRegion,
})
}
func (m *DatabasesModule) executeDynamoDbCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
m.executeCheck(check{
region: r,
wg: wg,
semaphore: semaphore,
dataReceiver: dataReceiver,
serviceMap: servicemap,
service: "dynamodb",
executor: m.getDynamoDBTablesPerRegion,
})
}
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) string {
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
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())
}
f := filepath.Join(path, "databases-UrlsOnly.txt")
var out string
@@ -321,12 +237,12 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) strin
out = out + fmt.Sprintln(database.Endpoint)
}
// err = os.WriteFile(f, []byte(out), 0644)
// if err != nil {
// m.modLog.Error(err.Error())
// m.CommandCounter.Error++
// panic(err.Error())
// }
err = os.WriteFile(f, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
panic(err.Error())
}
if verbosity > 2 {
fmt.Println()
@@ -337,8 +253,6 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) strin
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
return out
}
func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
@@ -355,127 +269,45 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
m.CommandCounter.Pending--
m.CommandCounter.Executing++
DBClusters, err := sdk.CachedRDSDescribeDBClusters(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, cluster := range DBClusters {
var public string
var service string
var roles string
if cluster.Endpoint == nil {
continue
}
name := aws.ToString(cluster.DBClusterIdentifier)
port := cluster.Port
endpoint := aws.ToString(cluster.Endpoint)
engine := aws.ToString(cluster.Engine)
if aws.ToBool(cluster.PubliclyAccessible) {
public = "True"
} else {
public = "False"
}
if isNeptune(cluster.Engine) {
service = "Neptune"
} else if isDocDB(cluster.Engine) {
service = "DocsDB"
} else {
service = "RDS"
}
associatedRoles := cluster.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(cluster.MasterUsername),
Port: aws.ToInt32(port),
Protocol: aws.ToString(cluster.Engine),
Public: public,
Roles: roles,
}
}
}
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 {
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.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
var public string
for _, instance := range DBInstances {
var public string
var service string
var roles string
if instance.Endpoint == nil {
continue
if instance.Endpoint != nil {
name := aws.ToString(instance.DBInstanceIdentifier)
port := instance.Endpoint.Port
endpoint := aws.ToString(instance.Endpoint.Address)
engine := aws.ToString(instance.Engine)
if instance.PubliclyAccessible {
public = "True"
} else {
public = "False"
}
dataReceiver <- Database{
AWSService: "RDS",
Region: r,
Name: name,
Engine: engine,
Endpoint: endpoint,
UserName: aws.ToString(instance.MasterUsername),
Port: port,
Protocol: aws.ToString(instance.Engine),
Public: public,
}
}
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) {
@@ -499,7 +331,7 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG
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++
@@ -511,13 +343,13 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG
name := aws.ToString(cluster.DBName)
//id := workspace.Id
endpoint := aws.ToString(cluster.Endpoint.Address)
port := aws.ToInt32(cluster.Endpoint.Port)
if aws.ToBool(cluster.PubliclyAccessible) {
if cluster.PubliclyAccessible {
public = "True"
} else {
public = "False"
}
port := cluster.Endpoint.Port
dataReceiver <- Database{
AWSService: awsService,
Region: r,
@@ -572,12 +404,48 @@ func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGrou
Endpoint: "N/A",
}
}
}
func isNeptune(engine *string) bool {
return *engine == "neptune"
}
func (m *DatabasesModule) getDocDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
func isDocDB(engine *string) bool {
return *engine == "docdb"
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
awsService := "DocDB"
Clusters, err := sdk.CachedDocDBDescribeDBClusters(m.DocDBClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, cluster := range Clusters {
name := aws.ToString(cluster.DBClusterIdentifier)
endpoint := aws.ToString(cluster.Endpoint)
port := aws.ToInt32(cluster.Port)
//size := aws.ToInt64(TableOutput.Table.TableSizeBytes)
userName := aws.ToString(cluster.MasterUsername)
dataReceiver <- Database{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
UserName: userName,
//Size: strconv.Itoa(int(size)),
}
}
}
-58
View File
@@ -1,58 +0,0 @@
package aws
import (
"path/filepath"
"strings"
"testing"
"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"
"github.com/aws/smithy-go/ptr"
"github.com/spf13/afero"
)
func TestDatabasesCommand(t *testing.T) {
m := DatabasesModule{
AWSProfile: "unittesting",
AWSRegions: []string{"us-east-1"},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
Account: aws.String("123456789012"),
},
Goroutines: 3,
WrapTable: false,
RDSClient: &sdk.MockedRDSClient{},
DynamoDBClient: &sdk.MockedAWSDynamoDBClient{},
RedshiftClient: &sdk.MockedRedshiftClient{},
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
tmpDir := ptr.ToString(internal.GetLogDirPath())
m.PrintDatabases(tmpDir, 2)
//resultsFile, err := afero.ReadFile(fs, "table/databases.txt")
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/databases.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFile, err)
}
expectedResults := []string{
"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 {
if !strings.Contains(string(resultsFile), expected) {
t.Errorf("Expected %s to be in the results file", expected)
}
}
}
-281
View File
@@ -1,281 +0,0 @@
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
}
+79 -124
View File
@@ -21,16 +21,13 @@ import (
type ECRModule struct {
// General configuration data
ECRClient sdk.AWSECRClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
ECRClient sdk.AWSECRClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Repositories []Repository
@@ -52,8 +49,8 @@ type Repository struct {
PolicyJSON string
}
func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ecr"
@@ -98,9 +95,8 @@ func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
receiverDone <- true
<-receiverDone
// This is the complete list of potential table columns
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Name",
@@ -110,53 +106,11 @@ func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
"ImageSize",
}
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Service",
"Region",
"Name",
"URI",
"PushedAt",
"ImageTags",
"ImageSize",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"URI",
"PushedAt",
"ImageTags",
"ImageSize",
}
}
// sort the table by Name
sort.Slice(m.Repositories, func(i, j int) bool {
return m.Repositories[i].Name < m.Repositories[j].Name
})
// Table rows
for i := range m.Repositories {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Repositories[i].AWSService,
m.Repositories[i].Region,
m.Repositories[i].Name,
@@ -170,7 +124,10 @@ func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -179,10 +136,9 @@ func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -198,12 +154,8 @@ 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ecr", r)
if err != nil {
@@ -287,89 +239,92 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
<-semaphore
}()
Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
var allImages []types.ImageDetail
var repoURI string
var repoName string
DescribeRepositories, err := m.describeRepositories(r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, repo := range Repositories {
repoName := aws.ToString(repo.RepositoryName)
repoURI := aws.ToString(repo.RepositoryUri)
for _, repo := range DescribeRepositories {
repoName = aws.ToString(repo.RepositoryName)
repoURI = aws.ToString(repo.RepositoryUri)
//created := *repo.CreatedAt
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
images, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
images, err := m.describeImages(r, repoName)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
continue
return
}
allImages = append(allImages, images...)
}
sort.Slice(images, func(i, j int) bool {
return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05")
})
sort.Slice(allImages, func(i, j int) bool {
return allImages[i].ImagePushedAt.Format("2006-01-02 15:04:05") < allImages[j].ImagePushedAt.Format("2006-01-02 15:04:05")
})
var image types.ImageDetail
var imageTags string
var image types.ImageDetail
var imageTags string
if len(images) > 1 {
image = images[len(images)-1]
} else if len(images) == 1 {
image = images[0]
} else {
continue
}
if len(allImages) > 1 {
image = allImages[len(allImages)-1]
} else if len(allImages) == 1 {
image = allImages[0]
} else {
return
}
if len(image.ImageTags) > 0 {
imageTags = image.ImageTags[0]
} else {
imageTags = "No tags"
}
if len(image.ImageTags) > 0 {
imageTags = image.ImageTags[0]
}
//imageTags := image.ImageTags[0]
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
imageSize := aws.ToInt64(image.ImageSizeInBytes)
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
//imageTags := image.ImageTags[0]
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
imageSize := aws.ToInt64(image.ImageSizeInBytes)
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
dataReceiver <- Repository{
AWSService: "ECR",
Name: repoName,
Region: r,
URI: pullURI,
PushedAt: pushedAt,
ImageTags: imageTags,
ImageSize: imageSize,
}
dataReceiver <- Repository{
AWSService: "ECR",
Name: repoName,
Region: r,
URI: pullURI,
PushedAt: pushedAt,
ImageTags: imageTags,
ImageSize: imageSize,
}
}
// func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) {
func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) {
// var repositories []types.Repository
// Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
// if err != nil {
// m.CommandCounter.Error++
// return nil, err
// }
var repositories []types.Repository
Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.CommandCounter.Error++
return nil, err
}
// repositories = append(repositories, Repositories...)
repositories = append(repositories, Repositories...)
// return repositories, nil
// }
return repositories, nil
}
// func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) {
// var images []types.ImageDetail
func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) {
var images []types.ImageDetail
// ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
// if err != nil {
// m.CommandCounter.Error++
// return nil, err
// }
// images = append(images, ImageDetails...)
// return images, nil
// }
ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
if err != nil {
m.CommandCounter.Error++
return nil, err
}
images = append(images, ImageDetails...)
return images, nil
}
func (m *ECRModule) getECRRepositoryPolicy(r string, repository string) (policy.Policy, error) {
var repoPolicy policy.Policy
+51 -22
View File
@@ -1,15 +1,52 @@
package aws
import (
"context"
"log"
"testing"
"time"
"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/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"}
type MockedECRClient struct {
}
func (m *MockedECRClient) DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) {
return &ecr.DescribeRepositoriesOutput{
Repositories: []types.Repository{
{
RepositoryName: aws.String("test1"),
RepositoryUri: aws.String("testURI"),
},
},
}, nil
}
func (m *MockedECRClient) DescribeImages(context.Context, *ecr.DescribeImagesInput, ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) {
return &ecr.DescribeImagesOutput{
ImageDetails: []types.ImageDetail{
{
ImagePushedAt: aws.Time(time.Now()),
ImageSizeInBytes: aws.Int64(123456),
ImageTags: []string{"latest"},
},
},
}, nil
}
func (m *MockedECRClient) GetRepositoryPolicy(context.Context, *ecr.GetRepositoryPolicyInput, ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) {
return &ecr.GetRepositoryPolicyOutput{
PolicyText: aws.String("policyText"),
}, nil
}
func TestDescribeRepos(t *testing.T) {
subtests := []struct {
@@ -24,37 +61,29 @@ func TestDescribeRepos(t *testing.T) {
outputDirectory: ".",
verbosity: 2,
testModule: ECRModule{
ECRClient: &sdk.MockedECRClient{},
ECRClient: &MockedECRClient{},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
Account: aws.String("123456789012"),
},
AWSProfile: "test",
Goroutines: 30,
AWSRegions: []string{"us-east-1"},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
},
expectedResult: []Repository{
{
Name: "repo1",
URI: "11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo1",
PushedAt: "2022-10-25 15:14:00",
ImageTags: "customtag, tag2",
ImageSize: 123456,
},
{
Name: "repo2",
URI: "11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo2",
PushedAt: "2021-10-15 11:14:00",
ImageTags: "latest",
ImageSize: 2222222,
}},
expectedResult: []Repository{{
Name: "test1",
URI: "testURI:latest",
PushedAt: "2022-10-25 15:14:06",
ImageTags: "latest",
ImageSize: 123456,
}},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintECR(subtest.outputDirectory, subtest.verbosity)
subtest.testModule.PrintECR(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedRepo := range subtest.expectedResult {
if expectedRepo.Name != subtest.testModule.Repositories[index].Name {
log.Fatal("Repo name does not match expected name")
+151 -128
View File
@@ -13,6 +13,7 @@ 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/ec2"
"github.com/aws/aws-sdk-go-v2/service/ecs"
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
@@ -20,22 +21,24 @@ import (
"github.com/sirupsen/logrus"
)
type DescribeTasksDefinitionAPIClient interface {
DescribeTaskDefinition(context.Context, *ecs.DescribeTaskDefinitionInput, ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
}
type ECSTasksModule struct {
ECSClient sdk.AWSECSClientInterface
EC2Client sdk.AWSEC2ClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
DescribeTaskDefinitionClient DescribeTasksDefinitionAPIClient
DescribeTasksClient ecs.DescribeTasksAPIClient
ListTasksClient ecs.ListTasksAPIClient
ListClustersClient ecs.ListClustersAPIClient
DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat 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
@@ -51,7 +54,6 @@ type MappedECSTask struct {
Cluster string
TaskDefinitionName string
TaskDefinitionContent string
ContainerName string
LaunchType string
ID string
ExternalIP string
@@ -61,7 +63,7 @@ type MappedECSTask struct {
CanPrivEsc string
}
func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) {
func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ecs-tasks"
@@ -76,8 +78,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.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(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))
@@ -124,7 +126,7 @@ func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) {
receiverDone <- true
<-receiverDone
m.printECSTaskData(outputDirectory, dataReceiver, verbosity)
m.printECSTaskData(outputFormat, outputDirectory, dataReceiver, verbosity)
}
@@ -141,40 +143,11 @@ func (m *ECSTasksModule) Receiver(receiver chan MappedECSTask, receiverDone chan
}
}
func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver chan MappedECSTask, verbosity int) {
// This is the complete list of potential table columns
m.output.Headers = []string{
"Account",
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"ID",
"External IP",
"Internal IP",
"RoleArn",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory string, dataReceiver chan MappedECSTask, verbosity int) {
if m.pmapperError == nil {
m.output.Headers = []string{
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"ID",
"External IP",
@@ -183,48 +156,61 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
m.output.Headers = []string{
"Cluster",
"TaskDefinition",
"ContainerName",
"LaunchType",
"ID",
"External IP",
"Internal IP",
"RoleArn",
"IsAdminRole?",
"CanPrivEscToAdmin?",
//"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?")
}
for _, ecsTask := range m.MappedECSTasks {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
ecsTask.Cluster,
ecsTask.TaskDefinitionName,
ecsTask.ContainerName,
ecsTask.LaunchType,
ecsTask.ID,
ecsTask.ExternalIP,
ecsTask.PrivateIP,
ecsTask.Role,
ecsTask.Admin,
ecsTask.CanPrivEsc,
},
)
if m.pmapperError == nil {
for _, ecsTask := range m.MappedECSTasks {
m.output.Body = append(
m.output.Body,
[]string{
ecsTask.Cluster,
ecsTask.TaskDefinitionName,
ecsTask.LaunchType,
ecsTask.ID,
ecsTask.ExternalIP,
ecsTask.PrivateIP,
ecsTask.Role,
ecsTask.Admin,
ecsTask.CanPrivEsc,
},
)
}
} else {
for _, ecsTask := range m.MappedECSTasks {
m.output.Body = append(
m.output.Body,
[]string{
ecsTask.Cluster,
ecsTask.TaskDefinitionName,
ecsTask.LaunchType,
ecsTask.ID,
ecsTask.ExternalIP,
ecsTask.PrivateIP,
ecsTask.Role,
ecsTask.Admin,
//ecsTask.CanPrivEsc,
},
)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -233,10 +219,9 @@ func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver c
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -308,12 +293,8 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedECSTask) {
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",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ecs", r)
if err != nil {
@@ -332,37 +313,75 @@ func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceive
func (m *ECSTasksModule) getListClusters(region string, dataReceiver chan MappedECSTask) {
ClusterArns, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
var PaginationControl *string
for {
ListClusters, err := m.ListClustersClient.ListClusters(
context.TODO(),
&(ecs.ListClustersInput{
NextToken: PaginationControl,
}),
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, clusterARN := range ClusterArns {
m.getListTasks(clusterARN, region, dataReceiver)
}
for _, clusterARN := range ListClusters.ClusterArns {
m.getListTasks(clusterARN, region, dataReceiver)
}
if ListClusters.NextToken != nil {
PaginationControl = ListClusters.NextToken
} else {
PaginationControl = nil
break
}
}
}
func (m *ECSTasksModule) getListTasks(clusterARN string, region string, dataReceiver chan MappedECSTask) {
TaskArns, err := sdk.CachedECSListTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
var PaginationControl *string
for {
batchSize := 100 // maximum value: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTasks.html#API_DescribeTasks_RequestSyntax
for i := 0; i < len(TaskArns); i += batchSize {
j := i + batchSize
if j > len(TaskArns) {
j = len(TaskArns)
ListTasks, err := m.ListTasksClient.ListTasks(
context.TODO(),
&(ecs.ListTasksInput{
Cluster: aws.String(clusterARN),
NextToken: PaginationControl,
}),
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
m.loadTasksData(clusterARN, TaskArns[i:j], region, dataReceiver)
}
batchSize := 100 // maximum value: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTasks.html#API_DescribeTasks_RequestSyntax
for i := 0; i < len(ListTasks.TaskArns); i += batchSize {
j := i + batchSize
if j > len(ListTasks.TaskArns) {
j = len(ListTasks.TaskArns)
}
m.loadTasksData(clusterARN, ListTasks.TaskArns[i:j], region, dataReceiver)
}
if ListTasks.NextToken != nil {
PaginationControl = ListTasks.NextToken
} else {
PaginationControl = nil
break
}
}
}
func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, region string, dataReceiver chan MappedECSTask) {
@@ -371,15 +390,24 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
return
}
Tasks, err := sdk.CachedECSDescribeTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN, taskARNs)
DescribeTasks, err := m.DescribeTasksClient.DescribeTasks(
context.TODO(),
&(ecs.DescribeTasksInput{
Cluster: aws.String(clusterARN),
Tasks: taskARNs,
}),
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
var eniIDs []string
for _, task := range Tasks {
eniIDs := []string{}
for _, task := range DescribeTasks.Tasks {
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
if eniID != "" {
eniIDs = append(eniIDs, eniID)
@@ -392,9 +420,8 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
return
}
for _, task := range Tasks {
//taskDefinition, err := m.describeTaskDefinition(aws.ToString(task.TaskDefinitionArn), region)
taskDefinition, err := sdk.CachedECSDescribeTaskDefinition(m.ECSClient, aws.ToString(m.Caller.Account), region, aws.ToString(task.TaskDefinitionArn))
for _, task := range DescribeTasks.Tasks {
taskDefinition, err := m.describeTaskDefinition(aws.ToString(task.TaskDefinitionArn), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
@@ -404,7 +431,6 @@ 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),
@@ -435,7 +461,7 @@ func getTaskDefinitionContent(taskDefinition types.TaskDefinition) string {
}
func (m *ECSTasksModule) describeTaskDefinition(taskDefinitionArn string, region string) (types.TaskDefinition, error) {
DescribeTaskDefinition, err := m.ECSClient.DescribeTaskDefinition(
DescribeTaskDefinition, err := m.DescribeTaskDefinitionClient.DescribeTaskDefinition(
context.TODO(),
&ecs.DescribeTaskDefinitionInput{
TaskDefinition: &taskDefinitionArn,
@@ -485,13 +511,20 @@ func (m *ECSTasksModule) loadPublicIPs(eniIDs []string, region string) (map[stri
if len(eniIDs) == 0 {
return eniPublicIPs, nil
}
NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region)
DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
context.TODO(),
&(ec2.DescribeNetworkInterfacesInput{
NetworkInterfaceIds: eniIDs,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return nil, fmt.Errorf("getting elastic network interfaces: %s", err)
}
for _, eni := range NetworkInterfaces {
for _, eni := range DescribeNetworkInterfaces.NetworkInterfaces {
eniPublicIPs[aws.ToString(eni.NetworkInterfaceId)] = getPublicIPOfElasticNetworkInterface(eni)
}
@@ -516,18 +549,8 @@ 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 {
var ips []string
ips := []string{}
for _, attachment := range task.Attachments {
if aws.ToString(attachment.Type) != "ElasticNetworkInterface" || aws.ToString(attachment.Status) != "ATTACHED" {
+252 -17
View File
@@ -1,15 +1,249 @@
package aws
import (
"context"
"encoding/json"
"log"
"os"
"testing"
"time"
"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/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/ecs"
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
const DESCRIBE_TASKS_TEST_FILE = "./test-data/describe-tasks.json"
const DESCRIBE_NETWORK_INTEFACES_TEST_FILE = "./test-data/describe-network-interfaces.json"
type ListTasks struct {
TaskArns []string `json:"taskArns"`
}
type DescribeTasks struct {
Tasks []struct {
Attachments []struct {
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
Details []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"details"`
} `json:"attachments"`
Attributes []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"attributes"`
AvailabilityZone string `json:"availabilityZone"`
ClusterArn string `json:"clusterArn"`
Connectivity string `json:"connectivity"`
ConnectivityAt string `json:"connectivityAt"`
Containers []struct {
ContainerArn string `json:"containerArn"`
TaskArn string `json:"taskArn"`
Name string `json:"name"`
Image string `json:"image"`
RuntimeID string `json:"runtimeId"`
LastStatus string `json:"lastStatus"`
NetworkBindings []interface{} `json:"networkBindings"`
NetworkInterfaces []struct {
AttachmentID string `json:"attachmentId"`
PrivateIpv4Address string `json:"privateIpv4Address"`
} `json:"networkInterfaces"`
HealthStatus string `json:"healthStatus"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
} `json:"containers"`
CPU string `json:"cpu"`
CreatedAt string `json:"createdAt"`
DesiredStatus string `json:"desiredStatus"`
EnableExecuteCommand bool `json:"enableExecuteCommand"`
Group string `json:"group"`
HealthStatus string `json:"healthStatus"`
LastStatus string `json:"lastStatus"`
LaunchType string `json:"launchType"`
Memory string `json:"memory"`
Overrides struct {
ContainerOverrides []struct {
Name string `json:"name"`
} `json:"containerOverrides"`
InferenceAcceleratorOverrides []interface{} `json:"inferenceAcceleratorOverrides"`
} `json:"overrides"`
PlatformVersion string `json:"platformVersion"`
PlatformFamily string `json:"platformFamily"`
PullStartedAt string `json:"pullStartedAt"`
PullStoppedAt string `json:"pullStoppedAt"`
StartedAt string `json:"startedAt"`
StartedBy string `json:"startedBy"`
Tags []interface{} `json:"tags"`
TaskArn string `json:"taskArn"`
TaskDefinitionArn string `json:"taskDefinitionArn"`
Version int `json:"version"`
EphemeralStorage struct {
SizeInGiB int `json:"sizeInGiB"`
} `json:"ephemeralStorage"`
} `json:"tasks"`
Failures []interface{} `json:"failures"`
}
type DescribeNetworkInterfaces struct {
NetworkInterfaces []struct {
Status string `json:"Status"`
MacAddress string `json:"MacAddress"`
SourceDestCheck bool `json:"SourceDestCheck"`
VpcID string `json:"VpcId"`
Description string `json:"Description"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
NetworkInterfaceID string `json:"NetworkInterfaceId"`
PrivateIPAddresses []struct {
PrivateDNSName string `json:"PrivateDnsName"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
Primary bool `json:"Primary"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"PrivateIpAddresses"`
RequesterManaged bool `json:"RequesterManaged"`
Ipv6Addresses []interface{} `json:"Ipv6Addresses"`
PrivateDNSName string `json:"PrivateDnsName,omitempty"`
AvailabilityZone string `json:"AvailabilityZone"`
Attachment struct {
Status string `json:"Status"`
DeviceIndex int `json:"DeviceIndex"`
AttachTime time.Time `json:"AttachTime"`
InstanceID string `json:"InstanceId"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
AttachmentID string `json:"AttachmentId"`
InstanceOwnerID string `json:"InstanceOwnerId"`
} `json:"Attachment"`
Groups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"Groups"`
SubnetID string `json:"SubnetId"`
OwnerID string `json:"OwnerId"`
TagSet []interface{} `json:"TagSet"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"NetworkInterfaces"`
}
func readTestFile(testFile string) []byte {
file, err := os.ReadFile(testFile)
if err != nil {
log.Fatalf("can't read file %s", testFile)
}
return file
}
type mockedListclustersClient struct {
}
func (c *mockedListclustersClient) ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
return &ecs.ListClustersOutput{ClusterArns: []string{
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster2",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster3",
}}, nil
}
type mockedListTasksClient struct{}
func (c *mockedListTasksClient) ListTasks(ctx context.Context, input *ecs.ListTasksInput, f ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
return &ecs.ListTasksOutput{TaskArns: []string{
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE",
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE",
}}, nil
}
type mockedDescribeTasksClient struct {
describeTasks DescribeTasks
}
func (c *mockedDescribeTasksClient) DescribeTasks(ctx context.Context, input *ecs.DescribeTasksInput, f ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_TASKS_TEST_FILE), &c.describeTasks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_TASKS_TEST_FILE)
}
var tasks []ecsTypes.Task
for _, mockedTask := range c.describeTasks.Tasks {
if mockedTask.ClusterArn == aws.ToString(input.Cluster) {
for _, inputTask := range input.Tasks {
if mockedTask.TaskArn == inputTask {
var attachments []ecsTypes.Attachment
for _, a := range mockedTask.Attachments {
var deets []ecsTypes.KeyValuePair
for _, detail := range a.Details {
deets = append(deets, ecsTypes.KeyValuePair{
Name: aws.String(detail.Name),
Value: aws.String(detail.Value)})
}
attachments = append(attachments, ecsTypes.Attachment{
Type: aws.String(a.Type),
Details: deets,
Id: aws.String(a.ID),
Status: aws.String(a.Status),
})
}
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,
})
}
}
}
}
return &ecs.DescribeTasksOutput{Tasks: tasks}, nil
}
type mockedDescribeNetworkInterfacesClient struct {
describeNetworkInterfaces DescribeNetworkInterfaces
}
func (c *mockedDescribeNetworkInterfacesClient) 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)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
}
for _, mockedNic := range c.describeNetworkInterfaces.NetworkInterfaces {
for _, inputNicID := range input.NetworkInterfaceIds {
if mockedNic.NetworkInterfaceID == inputNicID {
nics = append(nics, ec2types.NetworkInterface{
Association: &ec2types.NetworkInterfaceAssociation{
PublicIp: aws.String(mockedNic.Association.PublicIP),
},
NetworkInterfaceId: aws.String(mockedNic.NetworkInterfaceID)})
}
}
}
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
}
type mockedDescribeTaskDefinitionInterface struct {
}
func (c *mockedDescribeTaskDefinitionInterface) DescribeTaskDefinition(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, f ...func(o *ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) {
testTaskDefinition := ecsTypes.TaskDefinition{}
testTaskDefinition.TaskRoleArn = aws.String("test123")
return &ecs.DescribeTaskDefinitionOutput{TaskDefinition: &testTaskDefinition}, nil
}
func TestECSTasks(t *testing.T) {
subtests := []struct {
name string
@@ -23,34 +257,35 @@ 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")},
SkipAdminCheck: true,
Goroutines: 30,
EC2Client: &sdk.MockedEC2Client2{},
ECSClient: &sdk.MockedECSClient{},
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
SkipAdminCheck: true,
Goroutines: 30,
DescribeNetworkInterfacesClient: &mockedDescribeNetworkInterfacesClient{},
DescribeTasksClient: &mockedDescribeTasksClient{},
ListTasksClient: &mockedListTasksClient{},
ListClustersClient: &mockedListclustersClient{},
DescribeTaskDefinitionClient: &mockedDescribeTaskDefinitionInterface{},
//IAMSimulatePrincipalPolicyClient: &mockedDescribeTaskDefinitionsClient{},
},
expectedResult: []MappedECSTask{{
Cluster: "MyCluster",
ID: "74de0355a10a4f979ac495c14EXAMPLE",
ContainerName: "web",
ExternalIP: "203.0.113.12",
Role: "test123",
Cluster: "MyCluster",
ID: "74de0355a10a4f979ac495c14EXAMPLE",
ExternalIP: "203.0.113.12",
Role: "test123",
}},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.ECSTasks(subtest.outputDirectory, subtest.verbosity)
subtest.testModule.ECSTasks(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedTask := range subtest.expectedResult {
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")
}
}
})
}
+67 -85
View File
@@ -5,7 +5,6 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
@@ -22,17 +21,13 @@ type EKSModule struct {
EKSClient sdk.EKSClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat 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
@@ -57,8 +52,8 @@ type Cluster struct {
CanPrivEsc string
}
func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "eks"
@@ -74,8 +69,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.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(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,48 +122,10 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
}
}
// This is the complete list of potential table columns
m.output.Headers = []string{
"Account",
"Region",
"Name",
//"Endpoint",
"Public",
//"OIDC",
"NodeGroup",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Region",
"Name",
"Endpoint",
"Public",
"OIDC",
"NodeGroup",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
// add - if struct is not empty do this. otherwise, dont write anything.
if m.pmapperError == nil {
m.output.Headers = []string{
"Service",
"Region",
"Name",
//"Endpoint",
@@ -179,32 +136,58 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
"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?")
} else {
m.output.Headers = []string{
"Service",
"Region",
"Name",
//"Endpoint",
"Public",
//"OIDC",
"NodeGroup",
"Role",
"IsAdminRole?",
//"CanPrivEscToAdmin?",
}
}
// Table rows
for i := range m.Clusters {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Clusters[i].Region,
m.Clusters[i].Name,
//m.Clusters[i].Endpoint,
m.Clusters[i].Public,
//m.Clusters[i].OIDC,
m.Clusters[i].NodeGroup,
m.Clusters[i].Role,
m.Clusters[i].Admin,
m.Clusters[i].CanPrivEsc,
},
)
if m.pmapperError == nil {
m.output.Body = append(
m.output.Body,
[]string{
m.Clusters[i].AWSService,
m.Clusters[i].Region,
m.Clusters[i].Name,
//m.Clusters[i].Endpoint,
m.Clusters[i].Public,
//m.Clusters[i].OIDC,
m.Clusters[i].NodeGroup,
m.Clusters[i].Role,
m.Clusters[i].Admin,
m.Clusters[i].CanPrivEsc,
},
)
} else {
m.output.Body = append(
m.output.Body,
[]string{
m.Clusters[i].AWSService,
m.Clusters[i].Region,
m.Clusters[i].Name,
//m.Clusters[i].Endpoint,
m.Clusters[i].Public,
//m.Clusters[i].OIDC,
m.Clusters[i].NodeGroup,
m.Clusters[i].Role,
m.Clusters[i].Admin,
//m.Clusters[i].CanPrivEsc,
},
)
}
}
var seen []string
@@ -216,6 +199,10 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -224,10 +211,9 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -243,12 +229,8 @@ 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("eks", r)
if err != nil {
+2 -1
View File
@@ -86,6 +86,7 @@ func TestEks(t *testing.T) {
EKSClient: &MockedEKSClientInterface{},
//IAMSimulatePrincipalPolicyClient: iam.SimulatePrincipalPolicyAPIClient,
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
@@ -103,7 +104,7 @@ func TestEks(t *testing.T) {
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.EKS(subtest.outputDirectory, subtest.verbosity)
subtest.testModule.EKS(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedCluster := range subtest.expectedResult {
if expectedCluster.Name != subtest.testModule.Clusters[index].Name {
log.Fatal("Cluster name does not match expected value")
+59 -97
View File
@@ -1,16 +1,16 @@
package aws
import (
"context"
"fmt"
"os"
"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/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
@@ -18,16 +18,14 @@ import (
)
type ElasticNetworkInterfacesModule struct {
EC2Client sdk.AWSEC2ClientInterface
//EC2Client *ec2.Client
DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
AWSProfile string
WrapTable bool
MappedENIs []MappedENI
CommandCounter internal.CommandCounter
@@ -46,7 +44,7 @@ type MappedENI struct {
Description string
}
func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputDirectory string, verbosity int) {
func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputFormat string, outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "elastic-network-interfaces"
@@ -86,7 +84,7 @@ func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputDirector
receiverDone <- true
<-receiverDone
m.printENIsData(outputDirectory, dataReceiver, verbosity)
m.printENIsData(outputFormat, outputDirectory, dataReceiver, verbosity)
}
@@ -103,9 +101,8 @@ func (m *ElasticNetworkInterfacesModule) Receiver(receiver chan MappedENI, recei
}
}
func (m *ElasticNetworkInterfacesModule) printENIsData(outputDirectory string, dataReceiver chan MappedENI, verbosity int) {
func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outputDirectory string, dataReceiver chan MappedENI, verbosity int) {
m.output.Headers = []string{
"Account",
"ID",
"Type",
"External IP",
@@ -114,48 +111,10 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputDirectory string, d
"Attached Instance",
"Description",
}
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"ID",
"Type",
"External IP",
"Internal IP",
"VPC ID",
"Attached Instance",
"Description",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"ID",
"Type",
"External IP",
"Internal IP",
"VPC ID",
"Attached Instance",
"Description",
}
}
for _, eni := range m.MappedENIs {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
eni.ID,
eni.Type,
eni.ExternalIP,
@@ -168,7 +127,10 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputDirectory string, d
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -177,10 +139,9 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputDirectory string, d
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -234,12 +195,8 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedENI) {
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",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
@@ -256,45 +213,50 @@ func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGr
}
func (m *ElasticNetworkInterfacesModule) getDescribeNetworkInterfaces(region string, dataReceiver chan MappedENI) {
NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region)
// var PaginationControl *string
// for {
// DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
// context.TODO(),
// &(ec2.DescribeNetworkInterfacesInput{
// NextToken: PaginationControl,
// }),
// func(o *ec2.Options) {
// o.Region = region
// },
// )
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, eni := range NetworkInterfaces {
status := string(eni.Status)
if status == "available" {
continue // unused ENI
var PaginationControl *string
for {
DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
context.TODO(),
&(ec2.DescribeNetworkInterfacesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
mappedENI := MappedENI{
ID: aws.ToString(eni.NetworkInterfaceId),
Type: string(eni.InterfaceType),
ExternalIP: getPublicIPOfElasticNetworkInterface(eni),
PrivateIP: aws.ToString(eni.PrivateIpAddress),
VPCID: aws.ToString(eni.VpcId),
AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni),
Description: aws.ToString(eni.Description),
for _, eni := range DescribeNetworkInterfaces.NetworkInterfaces {
status := string(eni.Status)
if status == "available" {
continue // unused ENI
}
mappedENI := MappedENI{
ID: aws.ToString(eni.NetworkInterfaceId),
Type: string(eni.InterfaceType),
ExternalIP: getPublicIPOfElasticNetworkInterface(eni),
PrivateIP: aws.ToString(eni.PrivateIpAddress),
VPCID: aws.ToString(eni.VpcId),
AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni),
Description: aws.ToString(eni.Description),
}
dataReceiver <- mappedENI
}
dataReceiver <- mappedENI
}
if DescribeNetworkInterfaces.NextToken != nil {
PaginationControl = DescribeNetworkInterfaces.NextToken
} else {
PaginationControl = nil
break
}
}
}
func getPublicIPOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string {
+33 -44
View File
@@ -1,59 +1,48 @@
package aws
import (
"context"
"encoding/json"
"log"
"testing"
"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/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
func TestElasticNetworkInterfaces(t *testing.T) {
type mockedDescribeNetworkInterfacesClient2 struct {
describeNetworkInterfaces DescribeNetworkInterfaces
}
m := ElasticNetworkInterfacesModule{
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
EC2Client: &sdk.MockedEC2Client2{},
func (c *mockedDescribeNetworkInterfacesClient2) 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)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
}
//m.ElasticNetworkInterfaces("table", ".", 3)
subtests := []struct {
name string
testModule ElasticNetworkInterfacesModule
expectedResult []MappedENI
}{
{
name: "Test ElasticNetworkInterfaces",
testModule: m,
expectedResult: []MappedENI{
{
PrivateIP: "10.0.1.17",
ExternalIP: "203.0.113.12",
},
{
PrivateIP: "10.0.1.149",
ExternalIP: "198.51.100.0",
},
for _, mockednic := range c.describeNetworkInterfaces.NetworkInterfaces {
nics = append(nics, ec2types.NetworkInterface{
Association: &ec2types.NetworkInterfaceAssociation{
PublicIp: aws.String(mockednic.Association.PublicIP),
},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.ElasticNetworkInterfaces(".", 3)
for index, expectedTask := range subtest.expectedResult {
if expectedTask.ExternalIP != subtest.testModule.MappedENIs[index].ExternalIP {
t.Errorf("expected %s, got %s", expectedTask.ExternalIP, subtest.testModule.MappedENIs[index].ExternalIP)
}
if expectedTask.PrivateIP != subtest.testModule.MappedENIs[index].PrivateIP {
t.Errorf("expected %s, got %s", expectedTask.PrivateIP, subtest.testModule.MappedENIs[index].PrivateIP)
}
}
NetworkInterfaceId: aws.String(mockednic.NetworkInterfaceID),
PrivateIpAddress: aws.String(mockednic.PrivateIPAddress),
VpcId: aws.String(mockednic.VpcID),
Attachment: &ec2types.NetworkInterfaceAttachment{InstanceId: aws.String(mockednic.Attachment.InstanceID)},
Description: aws.String(mockednic.Description),
})
}
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
}
func TestElasticNetworkInterfaces(t *testing.T) {
m := ElasticNetworkInterfacesModule{
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
DescribeNetworkInterfacesClient: &mockedDescribeNetworkInterfacesClient2{},
}
m.ElasticNetworkInterfaces("table", ".", 3)
}
+359 -250
View File
@@ -55,15 +55,12 @@ type EndpointsModule struct {
AppRunnerClient *apprunner.Client
LightsailClient *lightsail.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Endpoints []Endpoint
@@ -86,8 +83,8 @@ type Endpoint struct {
var oe *smithy.OperationError
func (m *EndpointsModule) PrintEndpoints(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "endpoints"
@@ -106,7 +103,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 task status spinner/updated
//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
@@ -150,7 +147,6 @@ func (m *EndpointsModule) PrintEndpoints(outputDirectory string, verbosity int)
})
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Name",
@@ -160,48 +156,11 @@ func (m *EndpointsModule) PrintEndpoints(outputDirectory string, verbosity int)
"Public",
}
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Service",
"Region",
"Name",
"Endpoint",
"Port",
"Protocol",
"Public",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"Endpoint",
"Port",
"Protocol",
"Public",
}
}
// Table rows
for i := range m.Endpoints {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Endpoints[i].AWSService,
m.Endpoints[i].Region,
m.Endpoints[i].Name,
@@ -215,6 +174,10 @@ func (m *EndpointsModule) PrintEndpoints(outputDirectory string, verbosity int)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -223,10 +186,9 @@ func (m *EndpointsModule) PrintEndpoints(outputDirectory string, verbosity int)
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -268,12 +230,8 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
// <-semaphore
// }()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("lambda", r)
if err != nil {
@@ -293,7 +251,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("amazon-mq", r)
res, err = servicemap.IsServiceInRegion("mq", r)
if err != nil {
m.modLog.Error(err)
}
@@ -302,7 +260,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("opensearch-service", r)
res, err = servicemap.IsServiceInRegion("es", r)
if err != nil {
m.modLog.Error(err)
}
@@ -320,7 +278,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
wg.Add(1)
m.getGrafanaEndPointsPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("elasticloadbalancing", r)
res, err = servicemap.IsServiceInRegion("elb", r)
if err != nil {
m.modLog.Error(err)
}
@@ -333,7 +291,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("api-gateway", r)
res, err = servicemap.IsServiceInRegion("apigateway", r)
if err != nil {
m.modLog.Error(err)
}
@@ -378,7 +336,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("amazonlightsail.com", r)
res, err = servicemap.IsServiceInRegion("lightsail", r)
if err != nil {
m.modLog.Error(err)
}
@@ -452,7 +410,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++
@@ -508,7 +466,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++
@@ -581,13 +539,13 @@ 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++
continue
}
if aws.ToBool(BrokerDetails.PubliclyAccessible) {
if BrokerDetails.PubliclyAccessible {
public = "True"
} else {
public = "False"
@@ -634,27 +592,36 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s
for _, domainName := range DomainNames {
name := aws.ToString(domainName.DomainName)
DomainStatus, err := sdk.CachedOpenSearchDescribeDomain(m.OpenSearchClient, aws.ToString(m.Caller.Account), r, name)
//TODO: convert this to cacehd function
DomainNameDetails, err := m.OpenSearchClient.DescribeDomain(
context.TODO(),
&(opensearch.DescribeDomainInput{
DomainName: &name,
}),
func(o *opensearch.Options) {
o.Region = 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++
return
}
rawEndpoint := DomainStatus.Endpoint
raw_endpoint := DomainNameDetails.DomainStatus.Endpoint
var endpoint string
var kibanaEndpoint string
var kibana_endpoint string
// This exits the function if an opensearch domain exists but there is no endpoint
if rawEndpoint == nil {
// This exits thie function if an opensearch domain exists but there is no endpoint
if raw_endpoint == nil {
return
} else {
endpoint = fmt.Sprintf("https://%s", aws.ToString(rawEndpoint))
kibanaEndpoint = fmt.Sprintf("https://%s/_plugin/kibana/", aws.ToString(rawEndpoint))
endpoint = fmt.Sprintf("https://%s", aws.ToString(raw_endpoint))
kibana_endpoint = fmt.Sprintf("https://%s/_plugin/kibana/", aws.ToString(raw_endpoint))
}
//fmt.Println(endpoint)
@@ -685,7 +652,7 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s
AWSService: "OpenSearch",
Region: r,
Name: name,
Endpoint: kibanaEndpoint,
Endpoint: kibana_endpoint,
Port: 443,
Protocol: "https",
Public: public,
@@ -784,7 +751,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++
@@ -926,6 +893,10 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl2 *string
var PaginationControl3 *string
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
@@ -935,73 +906,108 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
return
}
GetDomainNames, err := sdk.CachedApiGatewayGetDomainNames(m.APIGatewayClient, 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.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, item := range GetDomainNames {
domain := aws.ToString(item.DomainName)
GetBasePathMappings, err := sdk.CachedApiGatewayGetBasePathMappings(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, item.DomainName)
for {
GetDomainNames, err := m.APIGatewayClient.GetDomainNames(
context.TODO(),
&apigateway.GetDomainNamesInput{
Position: PaginationControl2,
},
func(o *apigateway.Options) {
o.Region = 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++
break
}
for _, mapping := range GetBasePathMappings {
stage := aws.ToString(mapping.Stage)
basePath := aws.ToString(mapping.BasePath)
if basePath == "(none)" {
basePath = "" // Empty string since '/' is already prepended
}
for _, item := range GetDomainNames.Items {
for _, api := range Items {
if api.Id != nil && aws.ToString(api.Id) == aws.ToString(mapping.RestApiId) {
endpoints := m.getEndpointsPerAPIGateway(r, api)
for _, endpoint := range endpoints {
old := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.RestApiId), r, stage)
domain := aws.ToString(item.DomainName)
if strings.HasPrefix(endpoint.Endpoint, old) {
var new string
if basePath == "" {
new = fmt.Sprintf("https://%s/", domain)
} else {
new = fmt.Sprintf("https://%s/%s/", domain, basePath)
for {
GetBasePathMappings, err := m.APIGatewayClient.GetBasePathMappings(
context.TODO(),
&apigateway.GetBasePathMappingsInput{
DomainName: item.DomainName,
Position: PaginationControl3,
},
func(o *apigateway.Options) {
o.Region = 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.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, mapping := range GetBasePathMappings.Items {
stage := aws.ToString(mapping.Stage)
basePath := aws.ToString(mapping.BasePath)
if basePath == "(none)" {
basePath = "" // Empty string since '/' is already prepended
}
for _, api := range Items {
if api.Id != nil && aws.ToString(api.Id) == aws.ToString(mapping.RestApiId) {
endpoints := m.getEndpointsPerAPIGateway(r, api)
for _, endpoint := range endpoints {
old := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.RestApiId), r, stage)
if strings.HasPrefix(endpoint.Endpoint, old) {
var new string
if basePath == "" {
new = fmt.Sprintf("https://%s/", domain)
} else {
new = fmt.Sprintf("https://%s/%s/", domain, basePath)
}
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
}
}
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
break
}
}
}
if GetBasePathMappings.Position != nil {
PaginationControl3 = GetBasePathMappings.Position
} else {
PaginationControl3 = nil
break
}
}
}
if GetDomainNames.Position != nil {
PaginationControl2 = GetDomainNames.Position
} else {
PaginationControl2 = nil
break
}
}
}
func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.RestApi) []Endpoint {
var endpoints []Endpoint
var PaginationControl2 *string
awsService := "APIGateway"
var public string
name := aws.ToString(api.Name)
id := aws.ToString(api.Id)
rawEndpoint := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com", id, r)
raw_endpoint := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com", id, r)
var port int32 = 443
protocol := "https"
@@ -1013,47 +1019,72 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
public = "True"
}
GetStages, err := sdk.CachedApiGatewayGetStages(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
GetStages, err := m.APIGatewayClient.GetStages(
context.TODO(),
&apigateway.GetStagesInput{
RestApiId: &id,
},
func(o *apigateway.Options) {
o.Region = 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++
}
GetResources, err := sdk.CachedApiGatewayGetResources(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
for {
GetResources, err := m.APIGatewayClient.GetResources(
context.TODO(),
&apigateway.GetResourcesInput{
RestApiId: &id,
Position: PaginationControl2,
},
func(o *apigateway.Options) {
o.Region = 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()))
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.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, stage := range GetStages.Item {
stageName := aws.ToString(stage.StageName)
for _, resource := range GetResources {
if len(resource.ResourceMethods) != 0 {
path := aws.ToString(resource.Path)
for _, stage := range GetStages.Item {
stageName := aws.ToString(stage.StageName)
for _, resource := range GetResources.Items {
if len(resource.ResourceMethods) != 0 {
path := aws.ToString(resource.Path)
endpoint := fmt.Sprintf("%s/%s%s", rawEndpoint, stageName, path)
endpoint := fmt.Sprintf("%s/%s%s", raw_endpoint, stageName, path)
endpoints = append(endpoints, Endpoint{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
Protocol: protocol,
Public: public,
})
endpoints = append(endpoints, Endpoint{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
Protocol: protocol,
Public: public,
})
}
}
}
if GetResources.Position != nil {
PaginationControl2 = GetResources.Position
} else {
PaginationControl2 = nil
break
}
}
return endpoints
}
@@ -1102,6 +1133,10 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl2 *string
var PaginationControl3 *string
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
@@ -1111,136 +1146,210 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
return
}
GetDomainNames, err := sdk.CachedAPIGatewayv2GetDomainNames(m.APIGatewayv2Client, 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.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, item := range GetDomainNames {
domain := aws.ToString(item.DomainName)
GetApiMappings, err := sdk.CachedAPIGatewayv2GetApiMappings(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, domain)
for {
GetDomainNames, err := m.APIGatewayv2Client.GetDomainNames(
context.TODO(),
&apigatewayv2.GetDomainNamesInput{
NextToken: PaginationControl2,
},
func(o *apigatewayv2.Options) {
o.Region = 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++
break
}
for _, mapping := range GetApiMappings {
stage := aws.ToString(mapping.Stage)
if stage == "$default" {
stage = ""
}
path := aws.ToString(mapping.ApiMappingKey)
for _, item := range GetDomainNames.Items {
for _, api := range Items {
if api.ApiId != nil && aws.ToString(api.ApiId) == aws.ToString(mapping.ApiId) {
endpoints := m.getEndpointsPerAPIGatewayv2(r, api)
for _, endpoint := range endpoints {
var old string
if stage == "" {
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/", aws.ToString(mapping.ApiId), r)
} else {
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.ApiId), r, stage)
}
if strings.HasPrefix(endpoint.Endpoint, old) {
var new string
if path == "" {
new = fmt.Sprintf("https://%s/", domain)
} else {
new = fmt.Sprintf("https://%s/%s/", domain, path)
domain := aws.ToString(item.DomainName)
for {
GetApiMappings, err := m.APIGatewayv2Client.GetApiMappings(
context.TODO(),
&apigatewayv2.GetApiMappingsInput{
DomainName: item.DomainName,
NextToken: PaginationControl3,
},
func(o *apigatewayv2.Options) {
o.Region = 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.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, mapping := range GetApiMappings.Items {
stage := aws.ToString(mapping.Stage)
if stage == "$default" {
stage = ""
}
path := aws.ToString(mapping.ApiMappingKey)
for _, api := range Items {
if api.ApiId != nil && aws.ToString(api.ApiId) == aws.ToString(mapping.ApiId) {
endpoints := m.getEndpointsPerAPIGatewayv2(r, api)
for _, endpoint := range endpoints {
var old string
if stage == "" {
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/", aws.ToString(mapping.ApiId), r)
} else {
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.ApiId), r, stage)
}
if strings.HasPrefix(endpoint.Endpoint, old) {
var new string
if path == "" {
new = fmt.Sprintf("https://%s/", domain)
} else {
new = fmt.Sprintf("https://%s/%s/", domain, path)
}
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
}
}
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
break
}
}
}
if GetApiMappings.NextToken != nil {
PaginationControl3 = GetApiMappings.NextToken
} else {
PaginationControl3 = nil
break
}
}
}
if GetDomainNames.NextToken != nil {
PaginationControl2 = GetDomainNames.NextToken
} else {
PaginationControl2 = nil
break
}
}
}
func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2Types.Api) []Endpoint {
var endpoints []Endpoint
var PaginationControl2 *string
var PaginationControl3 *string
awsService := "APIGatewayv2"
var public string
name := aws.ToString(api.Name)
rawEndpoint := aws.ToString(api.ApiEndpoint)
raw_endpoint := aws.ToString(api.ApiEndpoint)
id := aws.ToString(api.ApiId)
var port int32 = 443
protocol := "https"
var stages []string
GetStages, err := sdk.CachedAPIGatewayv2GetStages(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, id)
for {
GetStages, err := m.APIGatewayv2Client.GetStages(
context.TODO(),
&apigatewayv2.GetStagesInput{
ApiId: &id,
NextToken: PaginationControl2,
},
func(o *apigatewayv2.Options) {
o.Region = 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.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, stage := range GetStages {
s := aws.ToString(stage.StageName)
if s == "$default" {
s = ""
}
stages = append(stages, s)
}
GetRoutes, err := sdk.CachedAPIGatewayv2GetRoutes(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, id)
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.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, stage := range stages {
for _, route := range GetRoutes {
routeKey := route.RouteKey
var path string
if len(strings.Fields(*routeKey)) == 2 {
path = strings.Fields(*routeKey)[1]
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())))
}
var endpoint string
if stage == "" {
endpoint = fmt.Sprintf("%s%s", rawEndpoint, path)
} else {
endpoint = fmt.Sprintf("%s/%s%s", rawEndpoint, stage, path)
}
public = "True"
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
endpoints = append(endpoints, Endpoint{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
Protocol: protocol,
Public: public,
})
for _, stage := range GetStages.Items {
s := aws.ToString(stage.StageName)
if s == "$default" {
s = ""
}
stages = append(stages, s)
}
if GetStages.NextToken != nil {
PaginationControl2 = GetStages.NextToken
} else {
PaginationControl2 = nil
break
}
}
for {
GetRoutes, err := m.APIGatewayv2Client.GetRoutes(
context.TODO(),
&apigatewayv2.GetRoutesInput{
ApiId: &id,
NextToken: PaginationControl3,
},
func(o *apigatewayv2.Options) {
o.Region = 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.modLog.Error(err.Error())
m.CommandCounter.Error++
continue
}
for _, stage := range stages {
for _, route := range GetRoutes.Items {
routeKey := route.RouteKey
var path string
if len(strings.Fields(*routeKey)) == 2 {
path = strings.Fields(*routeKey)[1]
}
var endpoint string
if stage == "" {
endpoint = fmt.Sprintf("%s%s", raw_endpoint, path)
} else {
endpoint = fmt.Sprintf("%s/%s%s", raw_endpoint, stage, path)
}
public = "True"
endpoints = append(endpoints, Endpoint{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
Protocol: protocol,
Public: public,
})
}
}
if GetRoutes.NextToken != nil {
PaginationControl3 = GetRoutes.NextToken
} else {
PaginationControl3 = nil
break
}
}
return endpoints
}
@@ -1262,7 +1371,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++
@@ -1277,7 +1386,7 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
endpoint := aws.ToString(instance.Endpoint.Address)
awsService := "RDS"
if aws.ToBool(instance.PubliclyAccessible) {
if instance.PubliclyAccessible {
public = "True"
} else {
public = "False"
@@ -1288,7 +1397,7 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
Region: r,
Name: name,
Endpoint: endpoint,
Port: aws.ToInt32(port),
Port: port,
Protocol: aws.ToString(instance.Engine),
Public: public,
}
@@ -1315,12 +1424,12 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG
awsService := "Redshift"
protocol := "https"
// 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).
// 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).
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++
@@ -1333,7 +1442,7 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG
//id := workspace.Id
endpoint := aws.ToString(cluster.Endpoint.Address)
if aws.ToBool(cluster.PubliclyAccessible) {
if cluster.PubliclyAccessible {
public = "True"
} else {
public = "False"
@@ -1346,7 +1455,7 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG
Region: r,
Name: name,
Endpoint: endpoint,
Port: aws.ToInt32(port),
Port: port,
Protocol: protocol,
Public: public,
}
@@ -1448,12 +1557,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 = "Cloudfront"
var protocol = "https"
var r = "Global"
var public = "True"
var awsService string = "Cloudfront"
var protocol string = "https"
var r string = "Global"
var public string = "True"
// 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).
// 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).
for {
ListDistributions, err := m.CloudfrontClient.ListDistributions(
context.TODO(),
@@ -1463,7 +1572,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++
@@ -1679,8 +1788,8 @@ func (m *EndpointsModule) getLightsailContainerEndpointsPerRegion(r string, wg *
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
var public = "True"
var protocol = "https"
var public string = "True"
var protocol string = "https"
var port int32 = 443
containerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r)
+69 -174
View File
@@ -6,7 +6,6 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
@@ -35,15 +34,12 @@ import (
type EnvsModule struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSProfile string
AWSOutputType string
AWSTableCols string
Goroutines int
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSProfile string
OutputFormat string
Goroutines int
WrapTable bool
// Service Clients
ECSClient *ecs.Client
@@ -63,16 +59,14 @@ type EnvsModule struct {
type EnvironmentVariable struct {
service string
taskDefinition string
name string
region string
environmentVarName string
environmentVarValue string
interesting bool
}
func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "env-vars"
@@ -125,7 +119,6 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
// Table headers
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Name",
@@ -133,77 +126,24 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
"Value",
}
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Service",
"Region",
"Name",
"Key",
"Value",
}
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"Key",
"Value",
}
}
m.determineIfInteresting()
//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,
finalName,
magenta(envVar.environmentVarName),
magenta(envVar.environmentVarValue),
},
)
} else {
m.output.Body = append(
m.output.Body, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
finalName,
envVar.environmentVarName,
envVar.environmentVarValue,
},
)
}
m.output.Body = append(
m.output.Body, []string{
envVar.service,
envVar.region,
envVar.name,
envVar.environmentVarName,
envVar.environmentVarValue,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -211,27 +151,13 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
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.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
})
// Create another table file that only contains the environment variables that seem to be secrets.
body := m.interestingEnvVarsOnly()
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: body,
TableCols: tableCols,
Name: fmt.Sprintf("%s-interesting", m.output.CallingModule),
SkipPrintToScreen: true,
})
o.WriteFullOutput(o.Table.TableFiles, nil)
fmt.Printf("[%s][%s] %s environment variables found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
@@ -241,20 +167,6 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
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 *EnvsModule) determineIfInteresting() {
for i, envVar := range m.EnvironmentVariables {
// If the environment variable name contains the word secret, key, token, or password, we'll make it magenta to make it stand out.
lowerEnvVar := strings.ToLower(envVar.environmentVarName)
if (strings.Contains(lowerEnvVar, "secret") && !strings.Contains(lowerEnvVar, "arn")) ||
strings.Contains(lowerEnvVar, "key") ||
strings.Contains(lowerEnvVar, "token") ||
strings.Contains(lowerEnvVar, "user") ||
strings.Contains(lowerEnvVar, "pass") {
m.EnvironmentVariables[i].interesting = true
}
}
}
func EnvVarsContains(element EnvironmentVariable, array []EnvironmentVariable) bool {
for _, v := range array {
if v == element {
@@ -281,12 +193,8 @@ 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, _ := servicemap.IsServiceInRegion("ecs", r)
if res {
@@ -310,7 +218,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("amazonlightsail.com", r)
res, err = servicemap.IsServiceInRegion("lightsail", r)
if err != nil {
m.modLog.Error(err)
}
@@ -320,7 +228,7 @@ func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
go m.getLightsailEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("sagemaker-ai", r)
res, err = servicemap.IsServiceInRegion("sagemaker", r)
if err != nil {
m.modLog.Error(err)
}
@@ -368,7 +276,7 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
break
}
for _, containerDefinition := range DescribeTaskDefinition.TaskDefinition.ContainerDefinitions {
m.getECSEnvironmentVariablesPerDefinition(*DescribeTaskDefinition.TaskDefinition, containerDefinition, region, dataReceiver)
m.getECSEnvironmentVariablesPerDefinition(containerDefinition, region, dataReceiver)
}
}
}
@@ -407,13 +315,12 @@ func (m *EnvsModule) getTaskDefinitionFamilies(region string) []string {
}
func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(taskDefinition ecsTypes.TaskDefinition, containerDefinition ecsTypes.ContainerDefinition, region string, dataReceiver chan EnvironmentVariable) {
func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(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),
@@ -552,22 +459,25 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
return
}
if len(ContainerServices) > 0 {
if err == nil {
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,
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,
}
}
}
}
}
}
}
}
@@ -686,16 +596,18 @@ 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,
}
}
}
}
@@ -747,18 +659,20 @@ 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,
}
}
}
}
@@ -837,22 +751,3 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
}
}
func (m *EnvsModule) interestingEnvVarsOnly() [][]string {
var interestingBody [][]string
for _, envVar := range m.EnvironmentVariables {
if envVar.interesting {
interestingBody = append(
interestingBody, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
envVar.name,
magenta(envVar.environmentVarName),
magenta(envVar.environmentVarValue),
},
)
}
}
return interestingBody
}
+19 -61
View File
@@ -7,7 +7,6 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
@@ -28,15 +27,12 @@ type FilesystemsModule struct {
EFSClient *efs.Client
FSxClient *fsx.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Filesystems []FilesystemObject
@@ -59,8 +55,8 @@ type FilesystemObject struct {
Permissions string
}
func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "filesystems"
@@ -111,7 +107,7 @@ func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity i
})
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Name",
"DNS Name",
@@ -121,48 +117,12 @@ func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity i
"Permissions",
}
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Region",
"Name",
"DNS Name",
//"IP",
"Mount Target",
"Policy",
"Permissions",
}
} else {
tableCols = []string{
"Region",
"Name",
"DNS Name",
//"IP",
"Mount Target",
"Policy",
"Permissions",
}
}
// Table rows
for i := range m.Filesystems {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Filesystems[i].AWSService,
m.Filesystems[i].Region,
m.Filesystems[i].Name,
m.Filesystems[i].DnsName,
@@ -177,7 +137,10 @@ func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity i
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -186,10 +149,9 @@ func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity i
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -219,12 +181,8 @@ 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("efs", r)
if err != nil {
-418
View File
@@ -1,418 +0,0 @@
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
}
-1
View File
@@ -1 +0,0 @@
package collectors
-283
View File
@@ -1,283 +0,0 @@
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, &currentRelationship); 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
}
@@ -1,44 +0,0 @@
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
}
@@ -1,11 +0,0 @@
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{},
}
-20
View File
@@ -1,20 +0,0 @@
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{}
}
@@ -1,5 +0,0 @@
package models
type Resource struct {
ARN string
}
-647
View File
@@ -1,647 +0,0 @@
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
}
-71
View File
@@ -1,71 +0,0 @@
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"]
}
}
-148
View File
@@ -1,148 +0,0 @@
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
}
+72 -147
View File
@@ -7,7 +7,6 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
@@ -21,21 +20,17 @@ import (
type IamSimulatorModule struct {
// General configuration data
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfileProvided string
AWSProfileStub string
WrapTable bool
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
SimulatorResults []SimulatorResult
CommandCounter internal.CommandCounter
IamSimulatorAdminCheckOnly bool
SimulatorResults []SimulatorResult
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
@@ -48,46 +43,44 @@ 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",
}
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()
)
func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputDirectory string, verbosity int) {
func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputFormat string, outputDirectory string, verbosity int) {
// These struct values are used by the output module
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "iam-simulator"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
var filename string
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
var actionList []string
var pmapperCommands []string
var pmapperOutFileName string
var inputArn string
if m.AWSProfileProvided == "" {
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
} else {
m.AWSProfileStub = m.AWSProfileProvided
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
wg := new(sync.WaitGroup)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
@@ -102,78 +95,41 @@ 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 != "" {
// The user specified a specific --principal and a specific --action
fmt.Printf("[%s][%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), principal, action)
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
fmt.Printf("[%s][%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), principal, action)
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
actionList = append(actionList, action)
// if user supplied a principal name without the arn, try to create the arn as a user and as a role and run both
if !strings.Contains(principal, "arn:") {
// try as a role
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
m.getPolicySimulatorResult((&inputArn), actionList, resource, dataReceiver)
// try as a user
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
m.getPolicySimulatorResult((&inputArn), actionList, resource, dataReceiver)
} else {
// the arn was supplied so just run it
m.getPolicySimulatorResult((&principal), actionList, resource, dataReceiver)
}
m.getPolicySimulatorResult((&principal), actionList, resource, dataReceiver)
} else {
// The user specified a specific --principal, but --action was empty
fmt.Printf("[%s][%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), principal)
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
// if user supplied a principal name without the arn, try to create the arn as a user and as a role and run both
if !strings.Contains(principal, "arn:") {
// try as a role
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
m.getPolicySimulatorResult((&inputArn), defaultActionNames, resource, dataReceiver)
// try as a user
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
m.getPolicySimulatorResult((&inputArn), defaultActionNames, resource, dataReceiver)
} else {
// the arn was supplied so just run it
m.getPolicySimulatorResult((&principal), defaultActionNames, resource, dataReceiver)
}
fmt.Printf("[%s][%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), principal)
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
m.getPolicySimulatorResult((&principal), defaultActionNames, resource, dataReceiver)
}
} else {
if action != "" {
// The did not specify a specific --principal, but they did specify an --action
fmt.Printf("[%s][%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), action)
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
fmt.Printf("[%s][%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), action)
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
actionList = append(actionList, action)
wg.Add(1)
m.getIAMUsers(wg, actionList, resource, dataReceiver)
wg.Add(1)
m.getIAMRoles(wg, actionList, resource, dataReceiver)
pmapperOutFileName = filepath.Join(filename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
if m.AWSProfileProvided != "" {
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfileProvided, action, resource, pmapperOutFileName))
} else {
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper query \"who can do %s with %s\" | tee %s\n", action, resource, pmapperOutFileName))
}
pmapperOutFileName = filepath.Join(m.output.FullFilename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
} else {
// Both --principal and --action are empty. Run in default mode!
fmt.Printf("[%s][%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
filename = m.output.CallingModule
fmt.Printf("[%s][%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.output.FullFilename = m.output.CallingModule
m.executeChecks(wg, resource, dataReceiver)
for _, action := range defaultActionNames {
pmapperOutFileName = filepath.Join(filename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
if m.AWSProfileProvided != "" {
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfileProvided, action, resource, pmapperOutFileName))
} else {
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper query \"who can do %s with %s\" | tee %s\n", action, resource, pmapperOutFileName))
}
pmapperOutFileName = filepath.Join(m.output.FullFilename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
}
}
@@ -193,36 +149,11 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
// Regardless of what options were selected, for now at least, we will always print the data using the output module (table/csv mode)
m.output.Headers = []string{
"Account",
"Service",
"Principal",
"Query",
}
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Principal",
"Query",
}
} else {
tableCols = []string{
"Principal",
"Query",
}
}
sort.Slice(m.SimulatorResults, func(i, j int) bool {
return m.SimulatorResults[i].Query < m.SimulatorResults[j].Query
})
@@ -232,7 +163,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.SimulatorResults[i].AWSService,
m.SimulatorResults[i].Principal,
m.SimulatorResults[i].Query,
},
@@ -240,8 +171,9 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -250,15 +182,14 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: filename,
Header: m.output.Headers,
Body: m.output.Body,
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.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)
fmt.Printf("[%s][%s] We suggest running the pmapper commands in the loot file to get the same information but taking privesc paths into account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
fmt.Printf("[%s][%s] We suggest running the pmapper commands in the loot file to get the same information but taking privesc paths into account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// fmt.Printf("[%s]\t\tpmapper --profile %s graph create\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
// for _, line := range pmapperCommands {
// fmt.Printf("[%s]\t\t%s", cyan(m.output.CallingModule), cyan(m.AWSProfile), line)
@@ -266,9 +197,9 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
m.writeLoot(o.Table.DirectoryName, verbosity, pmapperCommands)
} else if principal != "" || action != "" {
fmt.Printf("[%s][%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
fmt.Printf("[%s][%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
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)
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 *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pmapperCommands []string) {
@@ -281,11 +212,7 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
outFile := filepath.Join(path, "iam-simulator-pmapper-commands.txt")
var out string
if m.AWSProfileProvided != "" {
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfileProvided)
} else {
out = out + fmt.Sprintf("pmapper graph create\n")
}
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfile)
for _, line := range pmapperCommands {
out = out + line
}
@@ -297,12 +224,12 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), outFile)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), outFile)
}
@@ -357,9 +284,7 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
Decision: "",
}
} else {
if !m.IamSimulatorAdminCheckOnly {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}
@@ -399,9 +324,7 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
Decision: "",
}
} else {
if !m.IamSimulatorAdminCheckOnly {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}
@@ -450,7 +373,7 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"secretsmanager:GetSecretValue",
"ssm:GetParameters",
"ssm:GetDocument",
}
for {
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
@@ -463,9 +386,11 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
},
)
if err != nil {
m.modLog.Error(err)
//m.modLog.Error(err.Error())
TxtLogger.Println(err.Error())
m.CommandCounter.Error++
m.modLog.Errorf("Failed admin check on %s", aws.ToString(principal))
//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))
return false
}
+142 -145
View File
@@ -13,6 +13,7 @@ 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/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
@@ -23,19 +24,15 @@ import (
type InstancesModule struct {
// General configuration data
EC2Client sdk.AWSEC2ClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
EC2Client *ec2.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat 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
@@ -66,8 +63,8 @@ type MappedInstance struct {
CanPrivEsc string
}
func (m *InstancesModule) Instances(filter string, outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *InstancesModule) Instances(filter string, outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "instances"
@@ -95,8 +92,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.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(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))
@@ -150,9 +147,9 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo
// 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")
if m.UserDataAttributesOnly {
m.printInstancesUserDataAttributesOnly(outputDirectory, dataReceiver)
m.printInstancesUserDataAttributesOnly(outputFormat, outputDirectory, dataReceiver)
} else {
m.printGeneralInstanceData(outputDirectory, dataReceiver, verbosity)
m.printGeneralInstanceData(outputFormat, outputDirectory, dataReceiver, verbosity)
}
}
@@ -170,7 +167,7 @@ func (m *InstancesModule) Receiver(receiver chan MappedInstance, receiverDone ch
}
}
func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputDirectory string, dataReceiver chan MappedInstance) {
func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) {
defer func() {
m.output.CallingModule = "instances"
}()
@@ -220,39 +217,12 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputDirectory s
}
}
func (m *InstancesModule) printGeneralInstanceData(outputDirectory string, dataReceiver chan MappedInstance, verbosity int) {
func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance, verbosity int) {
// Prepare Table headers
//m.output.Headers = table.Row{
m.output.Headers = []string{
"Account",
//"ID",
"Name",
//"Arn",
"ID",
"Zone",
"State",
"External IP",
"Internal IP",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
if m.pmapperError == nil {
// 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, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
m.output.Headers = []string{
//"ID",
"Name",
//"Arn",
@@ -265,9 +235,8 @@ func (m *InstancesModule) printGeneralInstanceData(outputDirectory string, dataR
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
m.output.Headers = []string{
//"ID",
"Name",
//"Arn",
@@ -278,40 +247,59 @@ func (m *InstancesModule) printGeneralInstanceData(outputDirectory string, dataR
"Internal IP",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
//"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 _, instance := range m.MappedInstances {
m.output.Body = append(
m.output.Body,
//table.Row{
[]string{
aws.ToString(m.Caller.Account),
//instance.ID,
instance.Name,
//instance.Arn,
instance.ID,
instance.AvailabilityZone,
instance.State,
instance.ExternalIP,
instance.PrivateIP,
instance.Role,
instance.Admin,
instance.CanPrivEsc,
},
)
if m.pmapperError == nil {
for _, instance := range m.MappedInstances {
m.output.Body = append(
m.output.Body,
//table.Row{
[]string{
//instance.ID,
instance.Name,
//instance.Arn,
instance.ID,
instance.AvailabilityZone,
instance.State,
instance.ExternalIP,
instance.PrivateIP,
instance.Role,
instance.Admin,
instance.CanPrivEsc,
},
)
}
} else {
for _, instance := range m.MappedInstances {
m.output.Body = append(
m.output.Body,
//table.Row{
[]string{
//instance.ID,
instance.Name,
//instance.Arn,
instance.ID,
instance.AvailabilityZone,
instance.State,
instance.ExternalIP,
instance.PrivateIP,
instance.Role,
instance.Admin,
//instance.CanPrivEsc,
},
)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
////m.output.OutputSelector(outputFormat)
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
@@ -322,10 +310,9 @@ func (m *InstancesModule) printGeneralInstanceData(outputDirectory string, dataR
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -435,21 +422,13 @@ 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()
// 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",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
m.modLog.Errorf("Error checking if EC2 is available in region %s: %v", r, err)
m.CommandCounter.Error++
return
m.modLog.Error(err)
}
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--
@@ -457,23 +436,31 @@ 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)
}
}
func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, region string) (userData *string, err error) {
UserData, err := sdk.CachedEC2DescribeInstanceAttributeUserData(m.EC2Client, aws.ToString(m.Caller.Account), region, aws.ToString(instanceID))
Attributes, err := m.EC2Client.DescribeInstanceAttribute(
context.TODO(),
&ec2.DescribeInstanceAttributeInput{
InstanceId: instanceID,
Attribute: types.InstanceAttributeName("userData"),
},
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return nil, err
} else {
if UserData == "" {
if Attributes.UserData.Value == nil {
return aws.String("NoUserData"), nil
} else {
data, _ := base64.StdEncoding.DecodeString(UserData)
data, _ := base64.StdEncoding.DecodeString(*Attributes.UserData.Value)
return aws.String(string(data)), nil
}
}
@@ -483,27 +470,53 @@ func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, regio
func (m *InstancesModule) getDescribeInstances(instancesToSearch []string, region string, dataReceiver chan MappedInstance) {
// The "PaginationControl" value is nil when there's no more data to return.
var PaginationControl *string
for {
Instances, err := sdk.CachedEC2DescribeInstances(m.EC2Client, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, instance := range Instances {
if instancesToSearch[0] == "all" || internal.Contains(aws.ToString(instance.InstanceId), instancesToSearch) {
m.loadInstanceData(instance, region, dataReceiver)
DescribeInstances, err := m.EC2Client.DescribeInstances(
context.TODO(),
&(ec2.DescribeInstancesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, reservation := range DescribeInstances.Reservations {
accountId := reservation.OwnerId
for _, instance := range reservation.Instances {
if instancesToSearch[0] == "all" || internal.Contains(aws.ToString(instance.InstanceId), instancesToSearch) {
m.loadInstanceData(instance, region, accountId, dataReceiver)
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if DescribeInstances.NextToken != nil {
PaginationControl = DescribeInstances.NextToken
} else {
PaginationControl = nil
break
}
}
}
func (m *InstancesModule) loadInstanceData(instance types.Instance, region string, dataReceiver chan MappedInstance) {
func (m *InstancesModule) loadInstanceData(instance types.Instance, region string, accountId *string, dataReceiver chan MappedInstance) {
var profile string
var externalIP string
var name string = ""
var adminRole string = ""
var profileArn, profileName, name string
var roleArn string = ""
// The name is in a tag so we have to do this to grab the value from the right tag
for _, tag := range instance.Tags {
@@ -523,50 +536,34 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
if instance.IamInstanceProfile == nil {
profile = "NoInstanceProfile"
dataReceiver <- MappedInstance{
ID: aws.ToString(instance.InstanceId),
Name: aws.ToString(&name),
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)),
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
State: string(instance.State.Name),
ExternalIP: externalIP,
PrivateIP: aws.ToString(instance.PrivateIpAddress),
Profile: profile,
Role: "",
Region: region,
Admin: adminRole,
CanPrivEsc: "",
}
} else {
profileArn = aws.ToString(instance.IamInstanceProfile.Arn)
// This returns only the role name without the preceding forward slash.
profileARN := aws.ToString(instance.IamInstanceProfile.Arn)
profileID := aws.ToString(instance.IamInstanceProfile.Id)
profile = strings.Split(profileARN, "/")[len(strings.Split(profileARN, "/"))-1]
// Extracting instance profile name from ARN
profileName = strings.Split(profileArn, "/")[len(strings.Split(profileArn, "/"))-1]
// Describe the IAM instance profile
profileOutput, err := sdk.CachedIamGetInstanceProfile(m.IAMClient, aws.ToString(m.Caller.Account), profileName)
if err != nil {
m.modLog.Errorf("failed to get instance profile for %s, %v", profileArn, err)
}
for _, role := range profileOutput.Roles {
dataReceiver <- MappedInstance{
ID: aws.ToString(instance.InstanceId),
Name: aws.ToString(&name),
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)),
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
State: string(instance.State.Name),
ExternalIP: externalIP,
PrivateIP: aws.ToString(instance.PrivateIpAddress),
Profile: profile,
Role: aws.ToString(role.Arn),
Region: region,
Admin: adminRole,
CanPrivEsc: "",
if roles, ok := m.InstanceProfileToRolesMap[profileID]; ok {
for _, role := range roles {
roleArn = aws.ToString(role.Arn)
}
}
}
dataReceiver <- MappedInstance{
ID: aws.ToString(instance.InstanceId),
Name: aws.ToString(&name),
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(accountId), aws.ToString(instance.InstanceId)),
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
State: string(instance.State.Name),
ExternalIP: externalIP,
PrivateIP: aws.ToString(instance.PrivateIpAddress),
Profile: profile,
Role: roleArn,
Region: region,
Admin: adminRole,
CanPrivEsc: "",
}
}
func (m *InstancesModule) getRolesFromInstanceProfiles() {
+621 -1867
View File
File diff suppressed because it is too large. Load diff
+62 -86
View File
@@ -7,7 +7,6 @@ import (
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
@@ -26,21 +25,16 @@ type LambdasModule struct {
LambdaClient *lambda.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
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
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Main module data
Lambdas []Lambda
@@ -55,16 +49,15 @@ type Lambda struct {
Region string
Type string
Name string
Arn string
Role string
Admin string
CanPrivEsc string
Public string
}
func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string, verbosity int) {
// These struct values are used by the output module
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "lambda"
@@ -78,8 +71,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.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(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))
@@ -130,51 +123,23 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
<-receiverDone
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Region",
"Name",
"Arn",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// 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",
"Region",
"Arn",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
if m.pmapperError == nil {
m.output.Headers = []string{
"Service",
"Region",
"Name",
"Resource",
"Role",
"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?")
} else {
m.output.Headers = []string{
"Service",
"Region",
"Resource",
"Role",
"IsAdminRole?",
}
}
sort.Slice(m.Lambdas, func(i, j int) bool {
@@ -184,24 +149,41 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
// Table rows
for i := range m.Lambdas {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Lambdas[i].Region,
//m.Lambdas[i].Type,
m.Lambdas[i].Name,
m.Lambdas[i].Arn,
m.Lambdas[i].Role,
m.Lambdas[i].Admin,
m.Lambdas[i].CanPrivEsc,
//m.Lambdas[i].Public,
},
)
if m.pmapperError == nil {
m.output.Body = append(
m.output.Body,
[]string{
m.Lambdas[i].AWSService,
m.Lambdas[i].Region,
//m.Lambdas[i].Type,
m.Lambdas[i].Name,
m.Lambdas[i].Role,
m.Lambdas[i].Admin,
m.Lambdas[i].CanPrivEsc,
//m.Lambdas[i].Public,
},
)
} else {
m.output.Body = append(
m.output.Body,
[]string{
m.Lambdas[i].AWSService,
m.Lambdas[i].Region,
//m.Lambdas[i].Type,
m.Lambdas[i].Name,
m.Lambdas[i].Role,
m.Lambdas[i].Admin,
//m.Lambdas[i].CanPrivEsc,
//m.Lambdas[i].Public,
},
)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -210,10 +192,9 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -229,12 +210,8 @@ 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("lambda", r)
if err != nil {
@@ -323,14 +300,13 @@ func (m *LambdasModule) getLambdasPerRegion(r string, wg *sync.WaitGroup, semaph
}
for _, function := range functions {
arn := aws.ToString(function.FunctionArn)
//arn := aws.ToString(function.FunctionArn)
name := aws.ToString(function.FunctionName)
role := aws.ToString(function.Role)
dataReceiver <- Lambda{
AWSService: "Lambda",
Name: name,
Arn: arn,
Region: r,
Type: "",
Role: role,
+24 -61
View File
@@ -65,16 +65,13 @@ type NetworkPortsModule struct {
LightsailClient *lightsail.Client
RDSClient *rds.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
Verbosity int
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Verbosity int
// Main module data
IPv4_Private []NetworkService
@@ -139,8 +136,8 @@ var naclToSG = map[string]string{
var validSecurityGroupProtocols = []string{"-1", "tcp", "udp"}
func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
// These struct values are used by the output module
func (m *NetworkPortsModule) PrintNetworkPorts(outputFormat string, outputDirectory string) {
// These stuct values are used by the output module
m.output.Verbosity = m.Verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "network-ports"
@@ -187,7 +184,6 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Protocol",
@@ -195,36 +191,6 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
"Ports",
}
// 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",
"Service",
"Region",
"Protocol",
"Host",
"Ports",
}
} else {
tableCols = []string{
"Service",
"Region",
"Protocol",
"Host",
"Ports",
}
}
// Table rows
for _, arr := range [][]NetworkService{m.IPv4_Private, m.IPv4_Public, m.IPv6} {
for _, i := range arr {
@@ -232,7 +198,6 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
i.AWSService,
i.Region,
i.Protocol,
@@ -246,6 +211,9 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
if len(m.IPv4_Private) > 0 || len(m.IPv4_Public) > 0 || len(m.IPv6) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//internal.OutputSelector(m.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
Verbosity: m.Verbosity,
CallingModule: m.output.CallingModule,
@@ -254,10 +222,9 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -274,12 +241,8 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) {
func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan NetworkServices) {
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",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ec2", r)
@@ -318,7 +281,7 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.getElastiCacheNetworkPortsPerRegion(r, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("elasticloadbalancing", r)
res, err = servicemap.IsServiceInRegion("elb", r)
if err != nil {
m.modLog.Error(err)
}
@@ -327,7 +290,7 @@ func (m *NetworkPortsModule) executeChecks(r string, wg *sync.WaitGroup, dataRec
m.getLBNetworkPortsPerRegion(r, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("amazonlightsail.com", r)
res, err = servicemap.IsServiceInRegion("lightsail", r)
if err != nil {
m.modLog.Error(err)
}
@@ -779,7 +742,7 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR
// Memcached
if cluster.ConfigurationEndpoint != nil {
ipv4_private = addHost(ipv4_private, aws.ToString(cluster.ConfigurationEndpoint.Address))
tcpPortsInts = addPort(tcpPortsInts, aws.ToInt32(cluster.ConfigurationEndpoint.Port))
tcpPortsInts = addPort(tcpPortsInts, cluster.ConfigurationEndpoint.Port)
} else {
replicationGroupId := aws.ToString(cluster.ReplicationGroupId)
for _, group := range nodes {
@@ -788,7 +751,7 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR
// Primary
if g.PrimaryEndpoint != nil && !strContains(reportedClusters, aws.ToString(g.PrimaryEndpoint.Address)) {
ipv4_private = addHost(ipv4_private, aws.ToString(g.PrimaryEndpoint.Address))
tcpPortsInts = addPort(tcpPortsInts, aws.ToInt32(g.PrimaryEndpoint.Port))
tcpPortsInts = addPort(tcpPortsInts, g.PrimaryEndpoint.Port)
reportedClusters = addHost(reportedClusters, aws.ToString(g.PrimaryEndpoint.Address))
}
@@ -796,7 +759,7 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR
// Reader
if g.ReaderEndpoint != nil && !strContains(reportedClusters, aws.ToString(g.ReaderEndpoint.Address)) {
ipv4_private = addHost(ipv4_private, aws.ToString(g.ReaderEndpoint.Address))
tcpPortsInts = addPort(tcpPortsInts, aws.ToInt32(g.ReaderEndpoint.Port))
tcpPortsInts = addPort(tcpPortsInts, g.ReaderEndpoint.Port)
reportedClusters = addHost(reportedClusters, aws.ToString(g.ReaderEndpoint.Address))
}
@@ -806,7 +769,7 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR
if aws.ToString(m.CacheClusterId) == aws.ToString(cluster.CacheClusterId) {
if m.ReadEndpoint != nil {
ipv4_private = addHost(ipv4_private, aws.ToString(m.ReadEndpoint.Address))
tcpPortsInts = addPort(tcpPortsInts, aws.ToInt32(m.ReadEndpoint.Port))
tcpPortsInts = addPort(tcpPortsInts, m.ReadEndpoint.Port)
}
}
}
@@ -1084,7 +1047,7 @@ func (m *NetworkPortsModule) getRdsNetworkPortsPerRegion(r string, dataReceiver
for _, instance := range DBInstances {
if aws.ToString(instance.DBInstanceStatus) == "available" {
host := []string{aws.ToString(instance.Endpoint.Address)}
var port int32 = aws.ToInt32(instance.Endpoint.Port)
var port int32 = instance.Endpoint.Port
var groups []SecurityGroup
for _, group := range instance.VpcSecurityGroups {
+14 -49
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
@@ -20,14 +19,12 @@ type OrgModule struct {
OrganizationsClient sdk.OrganizationsClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
DescribeOrgOutput *types.Organization
OutputFormat string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
DescribeOrgOutput *types.Organization
// Main module data
Accounts []Account
@@ -55,8 +52,8 @@ type Account struct {
OrgId string
}
func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *OrgModule) PrintOrgAccounts(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
var err error
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
@@ -88,7 +85,6 @@ func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) {
}
m.output.Headers = []string{
"Account",
"Name",
"ID",
"isManagementAccount?",
@@ -96,44 +92,12 @@ func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) {
"Email",
}
// 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",
"ID",
"isManagementAccount?",
"Status",
"Email",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Name",
"ID",
"isManagementAccount?",
"Status",
"Email",
}
}
// Table rows
for i := range m.Accounts {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Accounts[i].Name,
m.Accounts[i].Id,
strconv.FormatBool(m.Accounts[i].isManagementAccount),
@@ -145,6 +109,8 @@ func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) {
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -153,10 +119,9 @@ func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) {
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -307,7 +272,7 @@ func (m *OrgModule) addOrgAccount() {
isManagementAccount: false,
Name: "This account",
Id: aws.ToString(m.Caller.Account),
Email: "Unknown",
Email: "Unkonwn",
Arn: aws.ToString(m.Caller.Arn),
Status: "ACTIVE",
})
+19 -192
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@@ -23,15 +22,12 @@ type OutboundAssumedRolesModule struct {
// General configuration data
CloudTrailClient *cloudtrail.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
OutboundAssumeRoleEntries []OutboundAssumeRoleEntry
@@ -51,7 +47,6 @@ type OutboundAssumeRoleEntry struct {
SourcePrincipal string
DestinationAccount string
DestinationPrincipal string
Action string
LogTimestamp string
}
@@ -119,26 +114,8 @@ 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
func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "outbound-assumed-roles"
@@ -184,68 +161,28 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
<-receiverDone
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Type",
//"Source Account",
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
// 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",
"Region",
"Type",
//"Source Account",
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Region",
"Type",
//"Source Account",
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
}
// Table rows
for i := range m.OutboundAssumeRoleEntries {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.OutboundAssumeRoleEntries[i].AWSService,
m.OutboundAssumeRoleEntries[i].Region,
m.OutboundAssumeRoleEntries[i].Type,
//m.OutboundAssumeRoleEntries[i].SourceAccount,
m.OutboundAssumeRoleEntries[i].SourcePrincipal,
//m.OutboundAssumeRoleEntries[i].DestinationAccount,
m.OutboundAssumeRoleEntries[i].DestinationPrincipal,
m.OutboundAssumeRoleEntries[i].Action,
m.OutboundAssumeRoleEntries[i].LogTimestamp,
},
)
@@ -254,6 +191,9 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -262,10 +202,9 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -295,24 +234,17 @@ 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()
// Use shared ServiceMap instance if provided, otherwise create a new one
servicemap := m.ServiceMap
if servicemap == nil {
servicemap = &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
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.getCrossAccountBatchGetImageEntriesPerRegion(r, wg, semaphore, dataReceiver)
m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
}
}
@@ -390,7 +322,6 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
SourcePrincipal: sourcePrincipal,
DestinationAccount: destinationAccount,
DestinationPrincipal: destinationPrincipal,
Action: "sts:AssumeRole",
LogTimestamp: logTimestamp,
}
}
@@ -408,107 +339,3 @@ 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
}
}
}
+43 -78
View File
@@ -11,7 +11,6 @@ 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"
@@ -22,14 +21,12 @@ type IamPermissionsModule struct {
// General configuration data
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
@@ -37,7 +34,7 @@ type IamPermissionsModule struct {
Users []GAADUser
Roles []GAADRole
Groups []GAADGroup
Rows []common.PermissionsRow
Rows []PermissionsRow
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
@@ -72,12 +69,25 @@ type GAADGroup struct {
InlinePolicies []types.PolicyDetail
}
func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbosity int, principal string) {
// These struct values are used by the output module
type PermissionsRow struct {
AWSService string
Type string
Name string
Arn string
PolicyType string
PolicyName string
Effect string
Action string
Resource string
Condition string
}
func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDirectory string, verbosity int, principal string) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "permissions"
filename := m.output.CallingModule
m.output.FullFilename = m.output.CallingModule
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
@@ -88,20 +98,19 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
fmt.Printf("[%s][%s] Enumerating IAM permissions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
if principal != "" {
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
m.output.FullFilename = 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",
//"Service",
"Type",
"Name",
"Arn",
//"Arn",
"Policy",
"Policy Name",
"Policy Arn",
"Effect",
"Action",
"Resource",
@@ -113,13 +122,12 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
//m.Rows[i].AWSService,
m.Rows[i].Type,
m.Rows[i].Name,
m.Rows[i].Arn,
//m.Rows[i].Arn,
m.Rows[i].PolicyType,
m.Rows[i].PolicyName,
m.Rows[i].PolicyArn,
m.Rows[i].Effect,
m.Rows[i].Action,
m.Rows[i].Resource,
@@ -131,6 +139,9 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector3(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -138,48 +149,10 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
Wrap: m.WrapTable,
},
}
// 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 != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{
"Account",
"Type",
//"Name",
"Arn",
"Policy",
"Policy Name",
"Policy Arn",
"Effect",
"Action",
"Resource",
"Condition"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{
"Type",
"Name",
//"Arn",
"Policy",
"Policy Name",
"Effect",
"Action",
"Resource",
"Condition",
}
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
TableCols: tableCols,
Body: m.output.Body,
Name: filename,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -193,10 +166,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)
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
@@ -284,7 +257,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 == "" {
@@ -409,10 +382,6 @@ 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
@@ -426,14 +395,13 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
}
m.Rows = append(
m.Rows,
common.PermissionsRow{
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Managed",
PolicyName: p.Name,
PolicyArn: p.Arn,
Effect: effect,
Action: action,
Resource: resource,
@@ -453,14 +421,13 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
}
m.Rows = append(
m.Rows,
common.PermissionsRow{
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Managed",
PolicyName: p.Name,
PolicyArn: p.Arn,
Effect: effect,
Action: "[NotAction] " + action,
Resource: resource,
@@ -499,14 +466,13 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline
}
m.Rows = append(
m.Rows,
common.PermissionsRow{
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Inline",
PolicyName: aws.ToString(inlinePolicy.PolicyName),
PolicyArn: aws.ToString(inlinePolicy.PolicyName),
Effect: effect,
Action: action,
Resource: resource,
@@ -525,14 +491,13 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline
}
m.Rows = append(
m.Rows,
common.PermissionsRow{
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Inline",
PolicyName: aws.ToString(inlinePolicy.PolicyName),
PolicyArn: aws.ToString(inlinePolicy.PolicyName),
Effect: effect,
Action: "[NotAction] " + action,
Resource: resource,
+36 -398
View File
@@ -1,52 +1,39 @@
package aws
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"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"
)
type PmapperModule struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
PmapperDataBasePath string
pmapperGraph graph.Graph[string, string]
Nodes []Node
Edges []Edge
CommandCounter internal.CommandCounter
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"`
@@ -55,54 +42,19 @@ type Edge struct {
}
type Node struct {
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
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
}
type AttachedPolicies struct {
@@ -124,16 +76,9 @@ 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"
}
}
@@ -159,39 +104,7 @@ func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] {
}
for _, edge := range m.Edges {
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)
}
_ = pmapperGraph.AddEdge(edge.Source, edge.Destination)
}
//internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration)
@@ -203,11 +116,7 @@ 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
}
}
@@ -219,11 +128,7 @@ 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
}
}
@@ -231,8 +136,8 @@ func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool {
return false
}
func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "pmapper"
@@ -258,39 +163,11 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int)
}
m.output.Headers = []string{
"Account",
"Principal Arn",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// 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",
"Principal Arn",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Principal Arn",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
}
//Table rows
var isAdmin, pathToAdmin string
for i := range m.Nodes {
@@ -309,7 +186,6 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int)
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Nodes[i].Arn,
isAdmin,
pathToAdmin,
@@ -320,39 +196,23 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int)
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
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,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
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)
o.WriteFullOutput(o.Table.TableFiles, nil)
//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)))
@@ -373,6 +233,9 @@ 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
}
@@ -384,136 +247,9 @@ 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 {
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")
}
e, n := generatePmapperDataBasePaths(accountID)
nodesFile, err := os.Open(n)
if err != nil {
@@ -534,101 +270,3 @@ 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
}
+25 -97
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"path/filepath"
"strconv"
"strings"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
@@ -17,20 +16,12 @@ type IamPrincipalsModule struct {
// General configuration data
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
SkipAdminCheck bool
iamSimClient IamSimulatorModule
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Users []User
@@ -49,8 +40,6 @@ type User struct {
Name string
AttachedPolicies []string
InlinePolicies []string
Admin string
CanPrivEsc string
}
type Group struct {
@@ -70,16 +59,13 @@ type Role struct {
Name string
AttachedPolicies []string
InlinePolicies []string
Admin string
CanPrivEsc string
}
func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
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,
})
@@ -89,9 +75,6 @@ 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)
@@ -109,106 +92,52 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
//fmt.Printf("\nAnalyzed Resources by Region\n\n")
m.output.Headers = []string{
"Account",
"Service",
"Type",
"Name",
"Arn",
"AttachedPolicies",
"InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// 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",
"Type",
"Name",
"Arn",
//"AttachedPolicies",
//"InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Type",
"Name",
"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?")
// "AttachedPolicies",
// "InlinePolicies",
}
//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{
aws.ToString(m.Caller.Account),
m.Users[i].AWSService,
m.Users[i].Type,
m.Users[i].Name,
m.Users[i].Arn,
strings.Join(m.Users[i].AttachedPolicies, " , "),
strings.Join(m.Users[i].InlinePolicies, " , "),
m.Users[i].Admin,
m.Users[i].CanPrivEsc,
// m.Users[i].AttachedPolicies,
// m.Users[i].InlinePolicies,
},
)
}
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{
aws.ToString(m.Caller.Account),
m.Roles[i].AWSService,
m.Roles[i].Type,
m.Roles[i].Name,
m.Roles[i].Arn,
strings.Join(m.Roles[i].AttachedPolicies, " , "),
strings.Join(m.Roles[i].InlinePolicies, " , "),
m.Roles[i].Admin,
m.Roles[i].CanPrivEsc,
// m.Roles[i].AttachedPolicies,
// m.Roles[i].InlinePolicies,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -217,10 +146,9 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
+18 -57
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/internal"
@@ -21,15 +20,12 @@ type RAMModule struct {
// General configuration data
RAMClient *ram.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
ServiceMap *awsservicemap.AwsServiceMap // Shared service map to avoid repeated HTTP requests
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Resources []Resource
@@ -48,8 +44,8 @@ type Resource struct {
ShareType string
}
func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
// These struct values are used by the output module
func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ram"
@@ -95,7 +91,7 @@ func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Service",
"Region",
"Share Name",
"Type",
@@ -103,44 +99,12 @@ func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
"Share Type",
}
// 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",
"Region",
"Share Name",
"Type",
"Owner",
"Share Type",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Share Name",
"Type",
"Owner",
"Share Type",
}
}
// Table rows
for i := range m.Resources {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Resources[i].AWSService,
m.Resources[i].Region,
m.Resources[i].Name,
m.Resources[i].Type,
@@ -152,7 +116,9 @@ func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -161,10 +127,9 @@ func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -179,12 +144,8 @@ func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
func (m *RAMModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Resource) {
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",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ram", r)
if err != nil {
+46 -610
View File
@@ -19,24 +19,13 @@ import (
)
type ResourceTrustsModule struct {
KMSClient *sdk.KMSClientInterface
APIGatewayClient *sdk.APIGatewayClientInterface
EC2Client *sdk.AWSEC2ClientInterface
OpenSearchClient *sdk.OpenSearchClientInterface
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []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
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
WrapTable bool
AWSProfile string
CloudFoxVersion string
Resources2 []Resource2
CommandCounter internal.CommandCounter
@@ -55,44 +44,29 @@ type Resource2 struct {
ResourcePolicySummary string
Public string
Interesting string
TrustedPrincipals string
TrustsCrossAccount string
TrustsAllAccounts string
HasConditions string
}
func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity int, includeKms bool) {
// These struct values are used by the output module
func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "resource-trusts"
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.AWSProfile, aws.ToString(m.Caller.Account)))
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
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))
// 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))
}
fmt.Printf("[%s][%s] Enumerating Resources with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: CodeBuild, ECR, EFS, Lambda, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
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 task status spinner/updated
//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
@@ -105,7 +79,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, includeKms)
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
wg.Add(1)
@@ -121,42 +95,13 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
//"Account ID",
"ARN",
"Public",
"Interesting",
"Resource Policy Summary",
}
// 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",
"ARN",
"Public",
"Interesting",
"Resource Policy Summary",
}
} else {
tableCols = []string{
//"Account ID",
"ARN",
"Public",
"Interesting",
"Resource Policy Summary",
}
}
// sort the table roles by Interesting
sort.Slice(m.Resources2, func(i, j int) bool {
return m.Resources2[j].Interesting > m.Resources2[i].Interesting
@@ -167,7 +112,7 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
//m.Resources2[i].AccountID,
m.Resources2[i].ARN,
m.Resources2[i].Public,
m.Resources2[i].Interesting,
@@ -177,6 +122,8 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
}
if len(m.output.Body) > 0 {
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -185,32 +132,28 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
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.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)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s resource policies found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), strconv.Itoa(len(m.output.Body)))
fmt.Printf("[%s][%s] %s resource policies found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), 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 resource policies found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
fmt.Printf("[%s][%s] No resource policies found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
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)
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 *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2, includeKms bool) {
func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
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",
}
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("sns", r)
if err != nil {
@@ -251,7 +194,6 @@ 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)
@@ -261,7 +203,6 @@ 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)
@@ -272,73 +213,6 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap
m.getEFSfilesystemPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("secrets-manager", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getSecretsManagerSecretsPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("glue", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
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) {
@@ -364,7 +238,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.WrapTable, m.AWSMFAToken)
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.WrapTable)
ListTopics, err := cloudFoxSNSClient.listTopics(r)
if err != nil {
@@ -374,7 +248,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
for _, t := range ListTopics {
var statementSummaryInEnglish string
var isInteresting = "No"
var isInteresting string = "No"
topic, err := cloudFoxSNSClient.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
if err != nil {
m.modLog.Error(err.Error())
@@ -384,10 +258,9 @@ 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() {
@@ -397,7 +270,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
topic.IsPublic = "No"
}
// If there is a resource policy, convert the resource policy into plain english
// If there is a resouce policy, convert the resource policy into plain english
if !topic.Policy.IsEmpty() {
for i, statement := range topic.Policy.Statement {
@@ -441,7 +314,7 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.AWSMFAToken)
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfile, m.CloudFoxVersion)
ListBuckets, err := sdk.CachedListBuckets(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account))
if err != nil {
@@ -452,7 +325,7 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s
for _, b := range ListBuckets {
var statementSummaryInEnglish string
var isInteresting string = "No"
bucket := &BucketRow{
bucket := &Bucket{
Arn: fmt.Sprintf("arn:aws:s3:::%s", aws.ToString(b.Name)),
}
name := aws.ToString(b.Name)
@@ -484,7 +357,7 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s
bucket.IsPublic = "No"
}
// If there is a resource policy, convert the resource policy into plain english
// If there is a resouce policy, convert the resource policy into plain english
if !bucket.Policy.IsEmpty() {
for i, statement := range bucket.Policy.Statement {
var prefix string = ""
@@ -524,7 +397,7 @@ func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGrou
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
ListQueues, err := cloudFoxSQSClient.listQueues(r)
if err != nil {
@@ -586,9 +459,9 @@ func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGro
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
DescribeRepositories, err := sdk.CachedECRDescribeRepositories(cloudFoxECRClient.ECRClient, aws.ToString(m.Caller.Account), r)
DescribeRepositories, err := cloudFoxECRClient.describeRepositories(r)
if err != nil {
m.modLog.Error(err.Error())
return
@@ -650,7 +523,7 @@ func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, w
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
ListProjects, err := sdk.CachedCodeBuildListProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r)
if err != nil {
@@ -724,7 +597,7 @@ func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitG
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
ListFunctions, err := cloudFoxLambdaClient.listFunctions(r)
if err != nil {
@@ -789,7 +662,7 @@ func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *s
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
ListFileSystems, err := sdk.CachedDescribeFileSystems(cloudFoxEFSClient.EFSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
@@ -845,445 +718,8 @@ func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *s
}
}
// getSecretsManagerSecretsPoliciesPerRegion retrieves the resource policies for all Secrets Manager secrets in the 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) getSecretsManagerSecretsPoliciesPerRegion(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 }()
cloudFoxSecretsManagerClient := InitSecretsManagerClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
ListSecrets, err := sdk.CachedSecretsManagerListSecrets(cloudFoxSecretsManagerClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
return
}
for _, s := range ListSecrets {
var isPublic string
var statementSummaryInEnglish string
var isInteresting string = "No"
secretPolicy, err := sdk.CachedSecretsManagerGetResourcePolicy(cloudFoxSecretsManagerClient, aws.ToString(s.ARN), r, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
continue
}
if secretPolicy.IsPublic() {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
isPublic = "No"
}
if !secretPolicy.IsEmpty() {
for i, statement := range secretPolicy.Statement {
prefix := ""
if len(secretPolicy.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(s.ARN),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(s.Name),
Region: r,
Interesting: isInteresting,
}
}
}
}
}
// 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--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxGlueClient := InitGlueClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
ResourcePolicies, err := sdk.CachedGlueGetResourcePolicies(cloudFoxGlueClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
return
}
for _, resourcePolicy := range ResourcePolicies {
var isPublic string
var statementSummaryInEnglish string
var isInteresting string = "No"
if resourcePolicy.IsPublic() {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
isPublic = "No"
}
if !resourcePolicy.IsEmpty() {
for _, statement := range resourcePolicy.Statement {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
resources := statement.Resource
for _, resource := range resources {
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: resource,
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: resource,
Region: r,
Interesting: isInteresting,
}
}
}
}
}
}
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
-262
View File
@@ -1,262 +0,0 @@
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) {
testCases := []struct {
name string
input string
expected bool
}{
{
name: "test1",
input: "Everyone can sqs:SendMessage & can sqs:ReceiveMessage",
expected: true,
},
{
name: "PrincipalOrgPaths",
input: "aws:PrincipalOrgPaths",
expected: true,
},
{
name: "Empty",
input: "",
expected: false,
},
{
name: "NotInteresting",
input: "sns.amazonaws.com can lambda:InvokeFunction",
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := isResourcePolicyInteresting(tc.input)
if actual != tc.expected {
t.Errorf("Expected %v but got %v for input %s", tc.expected, actual, tc.input)
}
})
}
}
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")
}
}
}
}
+262 -399
View File
@@ -1,19 +1,22 @@
package aws
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"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/iam"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/knownawsaccountslookup"
"github.com/sirupsen/logrus"
)
@@ -28,20 +31,12 @@ type RoleTrustsModule struct {
CommandCounter internal.CommandCounter
SkipAdminCheck bool
WrapTable bool
AWSOutputType string
AWSTableCols string
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
iamSimClient IamSimulatorModule
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Main module data
AnalyzedRoles []AnalyzedRole
RoleTrustTable []RoleTrustRow
vendors *knownawsaccountslookup.Vendors
AnalyzedRoles []AnalyzedRole
// Used to store output data for pretty printing
output internal.OutputData2
@@ -49,27 +44,49 @@ type RoleTrustsModule struct {
modLog *logrus.Entry
}
type RoleTrustRow struct {
RoleARN string
RoleName string
TrustedPrincipal string
TrustedService string
TrustedFederatedProvider string
TrustedFederatedSubject string
ExternalID string
IsAdmin string
CanPrivEsc string
}
type AnalyzedRole struct {
roleARN *string
trustsDoc policy.TrustPolicyDocument
trustsDoc trustPolicyDocument
// trustType string // UNUSED FIELD, PLEASE REVIEW
Admin string
CanPrivEsc string
}
func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int) {
type trustPolicyDocument struct {
Version string `json:"Version"`
Statement []RoleTrustStatementEntry `json:"Statement"`
}
type RoleTrustStatementEntry struct {
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Principal struct {
AWS ListOfPrincipals `json:"AWS"`
Service ListOfPrincipals `json:"Service"`
Federated ListOfPrincipals `json:"Federated"`
} `json:"Principal"`
Action string `json:"Action"`
Condition struct {
StringEquals struct {
StsExternalID string `json:"sts:ExternalId"`
SAMLAud string `json:"SAML:aud"`
OidcEksSub string `json:"OidcEksSub"`
OidcEksAud string `json:"OidcEksAud"`
CognitoAud string `json:"cognito-identity.amazonaws.com:aud"`
} `json:"StringEquals"`
StringLike struct {
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
OidcEksSub string `json:"OidcEksSub"`
OidcEksAud string `json:"OidcEksAud"`
} `json:"StringLike"`
ForAnyValueStringLike struct {
CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"`
} `json:"ForAnyValue:StringLike"`
} `json:"Condition"`
}
func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "role-trusts"
@@ -80,13 +97,10 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
localAdminMap := make(map[string]bool)
m.vendors = knownawsaccountslookup.NewVendorMap()
m.vendors.PopulateKnownAWSAccounts()
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.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(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 {
@@ -104,7 +118,6 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
}
}
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -116,35 +129,25 @@ func (m *RoleTrustsModule) PrintRoleTrusts(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)))
principalsHeader, principalsBody, principalTableCols := m.printPrincipalTrusts(outputDirectory)
principalsHeader, principalsBody := m.printPrincipalTrusts(outputFormat, outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: principalsHeader,
Body: principalsBody,
TableCols: principalTableCols,
Name: "role-trusts-principals",
})
rootPrincipalsHeader, rootPrincipalsBody, rootPrincipalTableCols := m.printPrincipalTrustsRootOnly(outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: rootPrincipalsHeader,
Body: rootPrincipalsBody,
TableCols: rootPrincipalTableCols,
Name: "role-trusts-principals-root-trusts-without-external-id",
SkipPrintToScreen: true,
})
servicesHeader, servicesBody, serviceTableCols := m.printServiceTrusts(outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: servicesHeader,
Body: servicesBody,
TableCols: serviceTableCols,
Name: "role-trusts-services",
Header: principalsHeader,
Body: principalsBody,
Name: "role-trusts-principals",
})
federatedHeader, federatedBody, federatedTableCols := m.printFederatedTrusts(outputDirectory)
servicesHeader, servicesBody := m.printServiceTrusts(outputFormat, outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: federatedHeader,
Body: federatedBody,
TableCols: federatedTableCols,
Name: "role-trusts-federated",
Header: servicesHeader,
Body: servicesBody,
Name: "role-trusts-services",
})
federatedHeader, federatedBody := m.printFederatedTrusts(outputFormat, outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: federatedHeader,
Body: federatedBody,
Name: "role-trusts-federated",
})
o.WriteFullOutput(o.Table.TableFiles, nil)
@@ -154,427 +157,251 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
if len(servicesBody) > 0 {
fmt.Printf("[%s][%s] %s service role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
if len(federatedBody) > 0 {
fmt.Printf("[%s][%s] %s federated role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]string, [][]string, []string) {
func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-principals"
header = []string{
"Account",
"Role Arn",
"Role Name",
"Trusted Principal",
"ExternalID",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// 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 != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Account", "Role Arn", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
var column1 string
if m.pmapperError == nil {
header = []string{
"Role",
"Trusted Principal",
"ExternalID",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
} else {
tableCols = []string{"Role Name", "Trusted Principal", "ExternalID", "IsAdmin?", "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?")
header = []string{
"Role",
"Trusted Principal",
"ExternalID",
"IsAdmin?",
//"CanPrivEscToAdmin?",
}
}
for _, role := range m.AnalyzedRoles {
for _, statement := range role.trustsDoc.Statement {
for _, principal := range statement.Principal.AWS {
//check to see if the accountID is known
if strings.Contains(principal, "arn:aws:iam::") || strings.Contains(principal, "root") {
accountID := strings.Split(principal, ":")[4]
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
if vendorName != "" {
principal = fmt.Sprintf("%s (%s)", principal, vendorName)
}
if outputFormat == "wide" {
column1 = aws.ToString(role.roleARN)
} else {
column1 = GetResourceNameFromArn(aws.ToString(role.roleARN))
}
column2 := principal
column3 := statement.Condition.StringEquals.StsExternalID
column4 := role.Admin
column5 := role.CanPrivEsc
if m.pmapperError == nil {
body = append(body, []string{column1, column2, column3, column4, column5})
} else {
body = append(body, []string{column1, column2, column3, column4})
}
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
// 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),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedPrincipal,
RoleTrustRow.ExternalID,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
return header, body, tableCols
}
// printPrincipalTrusts but only those that have a trusted principal that contains :root and also does not have an external ID
func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string) ([]string, [][]string, []string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-principals-root-trusts-without-external-id"
header = []string{
"Account",
"Role Arn",
"Role Name",
"Trusted Principal",
"ExternalID",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// 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 != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Account", "Role Arn", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{"Role Name", "Trusted Principal", "ExternalID", "IsAdmin?", "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?")
}
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 == nil {
accountID := strings.Split(principal, ":")[4]
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
if vendorName != "" {
principal = fmt.Sprintf("%s (%s)", principal, vendorName)
}
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedPrincipal,
RoleTrustRow.ExternalID,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
return header, body, tableCols
return header, body
// if len(m.output.Body) > 0 {
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
// ////m.output.OutputSelector(outputFormat)
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
// //internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
// } else {
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
// 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 *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string, [][]string, []string) {
func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-services"
header = []string{
"Account",
"Role Arn",
"Role Name",
"Trusted Service",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
// 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 != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Account", "Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
var column1 string
if m.pmapperError == nil {
header = []string{
"Role",
"Trusted Service",
"ExternalID",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
} else {
tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "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?")
header = []string{
"Role",
"Trusted Service",
"ExternalID",
"IsAdmin?",
//"CanPrivEscToAdmin?",
}
}
for _, role := range m.AnalyzedRoles {
for _, statement := range role.trustsDoc.Statement {
for _, service := range statement.Principal.Service {
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedService: service,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
if outputFormat == "wide" {
column1 = aws.ToString(role.roleARN)
} else {
column1 = GetResourceNameFromArn(aws.ToString(role.roleARN))
}
column2 := service
column3 := statement.Condition.StringEquals.StsExternalID
column4 := role.Admin
column5 := role.CanPrivEsc
if m.pmapperError == nil {
body = append(body, []string{column1, column2, column3, column4, column5})
} else {
body = append(body, []string{column1, column2, column3, column4})
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedService,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
}
// sort the rows based on column 2 (service)
sort.SliceStable(body, func(i, j int) bool {
return body[i][3] < body[j][3]
return body[i][1] < body[j][1]
})
return header, body, tableCols
//m.sortTrustsTablePerTrustedPrincipal()
return header, body
// if len(m.output.Body) > 0 {
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
// //m.output.OutputSelector(outputFormat)
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
// internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
// } else {
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
}
func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]string, [][]string, []string) {
func (m *RoleTrustsModule) printFederatedTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-federated"
header = []string{
"Account",
"Role Arn",
"Role Name",
"Trusted Provider",
"Trusted Subject",
"IsAdmin?",
"CanPrivEscToAdmin?",
var column1, column3, column2 string
if m.pmapperError == nil {
header = []string{
"Role",
"Trusted Provider",
"Trusted Subject",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
} else {
header = []string{
"Role",
"Trusted Provider",
"Trusted Subject",
"IsAdmin?",
//"CanPrivEscToAdmin?",
}
}
// 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 != "" {
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Account", "Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{"Role Name", "Trusted Provider", "Trusted Subject", "IsAdmin?", "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?")
}
for _, role := range m.AnalyzedRoles {
if outputFormat == "wide" {
column1 = aws.ToString(role.roleARN)
} else {
column1 = GetResourceNameFromArn(aws.ToString(role.roleARN))
}
for _, statement := range role.trustsDoc.Statement {
if len(statement.Principal.Federated) > 0 {
provider, subjects := parseFederatedTrustPolicy(statement)
for _, subject := range subjects {
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedFederatedProvider: provider,
TrustedFederatedSubject: subject,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedFederatedProvider,
RoleTrustRow.TrustedFederatedSubject,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
column2, column3 = parseFederatedTrustPolicy(statement)
column4 := role.Admin
column5 := role.CanPrivEsc
if m.pmapperError == nil {
body = append(body, []string{column1, column2, column3, column4, column5})
} else {
body = append(body, []string{column1, column2, column3, column4})
}
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
return header, body, tableCols
return header, body
// if len(m.output.Body) > 0 {
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
// //m.output.OutputSelector(outputFormat)
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
// internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
// } else {
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
}
func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string, []string) {
var provider string
var subjects []string
if len(statement.Principal.Federated) > 1 {
sharedLogger.Warnf("Multiple federated providers found in the trust policy. This is not currently supported. Please review the trust policy for specifics.")
provider = "Multiple Federated Providers"
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
}
switch {
// lets use the Federated field to determine the provider, then based on the provider we can grab the list of subjects
case strings.Contains(statement.Principal.Federated[0], "token.actions.githubusercontent.com"):
provider = "GitHub"
if len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
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...)
func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) {
var column2, column3 string
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")"
trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n")
if trustedRepos == "" {
column3 = "ALL REPOS!!!"
} else {
subjects = append(subjects, "ALL REPOS!!!")
column3 = trustedRepos
}
case strings.Contains(statement.Principal.Federated[0], "oidc.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 {
subjects = append(subjects, statement.Condition.StringEquals.OidcEksSub...)
} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
column2 = "Okta" // (" + statement.Principal.Federated[0] + ")"
}
column3 = "Not applicable"
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" {
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
if statement.Condition.StringEquals.OidcEksSub != "" {
column3 = statement.Condition.StringEquals.OidcEksSub
} else if statement.Condition.StringLike.OidcEksSub != "" {
column3 = statement.Condition.StringLike.OidcEksSub
} else {
subjects = append(subjects, "ALL SERVICE ACCOUNTS!!!")
column3 = "ALL SERVICE ACCOUNTS!"
}
// terraform case
case strings.Contains(statement.Principal.Federated[0], "app.terraform.io"):
provider = "Terraform Cloud"
if len(statement.Condition.StringLike.TerraformSub) > 0 {
subjects = append(subjects, statement.Condition.StringLike.TerraformSub...)
} else if len(statement.Condition.StringEquals.TerraformSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.TerraformSub...)
} else {
subjects = append(subjects, "ALL WORKSPACES")
}
// Azure AD case
case strings.Contains(statement.Principal.Federated[0], "http://sts.windows.net"):
provider = "Azure AD"
if len(statement.Condition.StringLike.AzureADIss) > 0 {
subjects = append(subjects, statement.Condition.StringLike.AzureADIss...)
} else if len(statement.Condition.StringEquals.AzureADIss) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.AzureADIss...)
} else {
subjects = append(subjects, "ALL ISSUERS")
}
///AWS SSO case
case strings.Contains(statement.Principal.Federated[0], "AWSSSO"):
//provider = "AWS SSO"
accountId := strings.Split(statement.Principal.Federated[0], ":")[4]
provider = fmt.Sprintf("AWSSSO-%s", accountId)
subjects = append(subjects, "Not applicable")
// okta case
case strings.Contains(statement.Principal.Federated[0], "Okta"):
provider = "Okta"
subjects = append(subjects, "Not applicable")
// cognito case
case statement.Principal.Federated[0] == "cognito-identity.amazonaws.com":
provider = "Cognito"
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
column2 = "Cognito" // (" + statement.Principal.Federated[0] + ")"
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
subjects = append(subjects, statement.Condition.ForAnyValueStringLike.CognitoAMR)
} else {
subjects = append(subjects, "ALL IDENTITIES")
column3 = statement.Condition.ForAnyValueStringLike.CognitoAMR
}
// google workspace case
case strings.Contains(statement.Principal.Federated[0], "workspace.google.com"):
provider = "Google Workspace"
if len(statement.Condition.StringLike.GoogleWorkspaceSub) > 0 {
subjects = append(subjects, statement.Condition.StringLike.GoogleWorkspaceSub...)
} else if len(statement.Condition.StringEquals.GoogleWorkspaceSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.GoogleWorkspaceSub...)
} else {
subjects = append(subjects, "ALL USERS")
} else {
if column2 == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") {
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
column3 = "ALL SERVICE ACCOUNTS!"
} else if column2 == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
}
// GCP case
case strings.Contains(statement.Principal.Federated[0], "accounts.google.com"):
provider = "GCP"
if len(statement.Condition.StringLike.GCPSub) > 0 {
subjects = append(subjects, statement.Condition.StringLike.GCPSub...)
} else if len(statement.Condition.StringEquals.GCPSub) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.GCPSub...)
} else {
subjects = append(subjects, "ALL USERS")
}
// auth0 case
//not ready yet
// case strings.Contains(statement.Principal.Federated[0], "auth0.com"):
// provider = "Auth0"
// if len(statement.Condition.ForAnyValueStringLike.Auth0Amr) > 0 {
// subjects = append(subjects, statement.Condition.ForAnyValueStringLike.Auth0Amr...)
// } else {
// subjects = append(subjects, "ALL GROUPS")
// }
// circleci case
case strings.Contains(statement.Principal.Federated[0], "oidc.circleci.com"):
provider = "CircleCI"
if len(statement.Condition.StringLike.CircleCIAud) > 0 {
subjects = append(subjects, statement.Condition.StringLike.CircleCIAud...)
} else if len(statement.Condition.StringEquals.CircleCIAud) > 0 {
subjects = append(subjects, statement.Condition.StringEquals.CircleCIAud...)
} 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"
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
}
return provider, subjects
return column2, column3
}
func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
@@ -594,7 +421,7 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
}
for _, role := range ListRoles {
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
trustsdoc, err := parseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
@@ -608,9 +435,45 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
Admin: "",
CanPrivEsc: "",
})
}
}
}
func parseRoleTrustPolicyDocument(role types.Role) (trustPolicyDocument, error) {
document, _ := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument))
// These next six lines are a hack, needed because the EKS OIDC json field name is dynamic
// and therefore can't be used to unmarshall in a predictable way. The hack involves replacing
// the random pattern with a predictable one so that we can add the predictable one in the struct
// used to unmarshall.
pattern := `(\w+)\:`
pattern2 := `".[a-zA-Z0-9\-\.]+/id/`
var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub")
var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud")
document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub")
document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud")
var parsedDocumentToJSON trustPolicyDocument
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
return parsedDocumentToJSON, nil
}
// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string.
// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex
type ListOfPrincipals []string
func (r *ListOfPrincipals) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err == nil {
*r = append(*r, s)
return nil
}
var ss []string
if err := json.Unmarshal(b, &ss); err == nil {
*r = ss
return nil
}
return errors.New("cannot unmarshal neither to a string nor a slice of strings")
}
+57 -78
View File
@@ -1,28 +1,26 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"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/route53"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type Route53Module struct {
// General configuration data
Route53Client sdk.AWSRoute53ClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Route53Client *route53.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
@@ -44,9 +42,9 @@ type Record struct {
PrivateZone string
}
func (m *Route53Module) PrintRoute53(outputDirectory string, verbosity int) {
func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string, verbosity int) {
// These struct values are used by the output module
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "route53"
@@ -62,48 +60,19 @@ func (m *Route53Module) PrintRoute53(outputDirectory string, verbosity int) {
m.getRoute53Records()
m.output.Headers = []string{
"Account",
"Service",
"Name",
"Type",
"Value",
"PrivateZone",
}
// 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",
"Type",
"Value",
"PrivateZone",
}
} else {
tableCols = []string{
"Name",
"Type",
"Value",
"PrivateZone",
}
}
// Table rows
for i := range m.Records {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Records[i].AWSService,
m.Records[i].Name,
m.Records[i].Type,
m.Records[i].Value,
@@ -115,7 +84,10 @@ func (m *Route53Module) PrintRoute53(outputDirectory string, verbosity int) {
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -124,10 +96,9 @@ func (m *Route53Module) PrintRoute53(outputDirectory string, verbosity int) {
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -156,7 +127,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
var route53APublicRecords string
for _, record := range m.Records {
if record.Type == "A" || record.Type == "AAAA" || record.Type == "CNAME" {
if record.Type == "A" || record.Type == "AAAA" {
if record.PrivateZone == "True" {
route53APrivateRecords = route53APrivateRecords + fmt.Sprintln(record.Name)
} else {
@@ -205,47 +176,48 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
func (m *Route53Module) getRoute53Records() {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var recordName string
var recordType string
HostedZones, err := sdk.CachedRoute53ListHostedZones(m.Route53Client, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
var privateZone string
for _, zone := range HostedZones {
if zone.Config.PrivateZone {
privateZone = "True"
} else {
privateZone = "False"
}
Records, err := sdk.CachedRoute53ListResourceRecordSets(m.Route53Client, aws.ToString(m.Caller.Account), aws.ToString(zone.Id))
for {
ListHostedZones, err := m.Route53Client.ListHostedZones(
context.TODO(),
&route53.ListHostedZonesInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, record := range Records {
recordName = aws.ToString(record.Name)
recordType = string(record.Type)
if record.AliasTarget != nil {
m.Records = append(
m.Records,
Record{
AWSService: "Route53",
Name: recordName,
Type: fmt.Sprintf("Alias[%s]", recordType),
Value: aws.ToString(record.AliasTarget.DNSName),
PrivateZone: privateZone,
})
var privateZone string
for _, zone := range ListHostedZones.HostedZones {
id := aws.ToString(zone.Id)
if zone.Config.PrivateZone {
privateZone = "True"
} else {
privateZone = "False"
}
ListResourceRecordSets, err := m.Route53Client.ListResourceRecordSets(
context.TODO(),
&route53.ListResourceRecordSetsInput{
HostedZoneId: &id,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, record := range ListResourceRecordSets.ResourceRecordSets {
recordName = aws.ToString(record.Name)
recordType = string(record.Type)
for _, resourceRecord := range record.ResourceRecords {
recordValue := resourceRecord.Value
m.Records = append(
@@ -257,11 +229,18 @@ func (m *Route53Module) getRoute53Records() {
Value: aws.ToString(recordValue),
PrivateZone: privateZone,
})
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if ListHostedZones.NextMarker != nil {
PaginationControl = ListHostedZones.NextMarker
} else {
PaginationControl = nil
break
}
}
}
+3 -463
View File
@@ -6,85 +6,27 @@ import (
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"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 {
GetRestApis(context.Context, *apigateway.GetRestApisInput, ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error)
GetStages(context.Context, *apigateway.GetStagesInput, ...func(*apigateway.Options)) (*apigateway.GetStagesOutput, error)
GetResources(context.Context, *apigateway.GetResourcesInput, ...func(*apigateway.Options)) (*apigateway.GetResourcesOutput, error)
GetDomainNames(context.Context, *apigateway.GetDomainNamesInput, ...func(*apigateway.Options)) (*apigateway.GetDomainNamesOutput, error)
GetBasePathMappings(context.Context, *apigateway.GetBasePathMappingsInput, ...func(*apigateway.Options)) (*apigateway.GetBasePathMappingsOutput, error)
GetMethod(context.Context, *apigateway.GetMethodInput, ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error)
GetUsagePlans(context.Context, *apigateway.GetUsagePlansInput, ...func(*apigateway.Options)) (*apigateway.GetUsagePlansOutput, error)
GetUsagePlanKeys(context.Context, *apigateway.GetUsagePlanKeysInput, ...func(*apigateway.Options)) (*apigateway.GetUsagePlanKeysOutput, error)
}
type CachedGetMethodOutput struct {
ApiKeyRequired *bool
AuthorizationScopes []string
AuthorizationType *string
AuthorizerId *string
HttpMethod *string
MethodIntegration *apiGatewayTypes.Integration
MethodResponses map[string]apiGatewayTypes.MethodResponse
OperationName *string
RequestModels map[string]string
RequestParameters map[string]bool
RequestValidatorId *string
ResourceId *string
}
type CachedGetStagesOutput struct {
Item []apiGatewayTypes.Stage
}
func init() {
func RegisterApiGatewayTypes() {
gob.Register([]apiGatewayTypes.RestApi{})
gob.Register(CachedGetStagesOutput{})
gob.Register([]apiGatewayTypes.Resource{})
gob.Register([]apiGatewayTypes.DomainName{})
gob.Register([]apiGatewayTypes.BasePathMapping{})
gob.Register(CachedGetMethodOutput{})
gob.Register([]apiGatewayTypes.UsagePlan{})
gob.Register([]apiGatewayTypes.UsagePlanKey{})
}
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
// create a 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(
@@ -109,408 +51,6 @@ func CachedApiGatewayGetRestAPIs(client APIGatewayClientInterface, accountID str
}
PaginationControl = GetRestApis.Position
}
internal.Cache.Set(cacheKey, restAPIs, cache.DefaultExpiration)
return restAPIs, nil
}
// create a CachedApiGatewayGetStages function that accepts a client, account id, region, and rest api id. Make sure it handles caching, the region option and pagination
func CachedApiGatewayGetStages(client APIGatewayClientInterface, accountID string, region string, restAPIID string) (*apigateway.GetStagesOutput, error) {
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(),
&apigateway.GetStagesInput{
RestApiId: &restAPIID,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return &apigateway.GetStagesOutput{}, err
}
// Convert GetStagesOutput to CachedGetStagesOutput before caching
cachedVersion := toCachedGetStagesOutput(GetStages)
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
return GetStages, err
}
// Convert from AWS SDK type to custom type for caching
func toCachedGetStagesOutput(gso *apigateway.GetStagesOutput) *CachedGetStagesOutput {
return &CachedGetStagesOutput{
Item: gso.Item,
}
}
// Convert back to AWS SDK type after fetching from cache
func fromCachedGetStagesOutput(cgso *CachedGetStagesOutput) *apigateway.GetStagesOutput {
return &apigateway.GetStagesOutput{
Item: cgso.Item,
// Initialize ResultMetadata or leave it as zero value if it's not required for your use case
}
}
// create a CachedApiGatewayGetResources function that accepts a client, account id, region, and rest api id. Make sure it handles caching, the region option and pagination
func CachedApiGatewayGetResources(client APIGatewayClientInterface, accountID string, region string, restAPIID string) ([]apiGatewayTypes.Resource, error) {
var PaginationControl *string
var resources []apiGatewayTypes.Resource
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(
context.TODO(),
&apigateway.GetResourcesInput{
RestApiId: &restAPIID,
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return resources, err
}
resources = append(resources, GetResources.Items...)
//pagination
if GetResources.Position == nil {
break
}
PaginationControl = GetResources.Position
}
internal.Cache.Set(cacheKey, resources, cache.DefaultExpiration)
return resources, nil
}
// create a CachedApiGatewayGetDomainNames function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetDomainNames(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.DomainName, error) {
var PaginationControl *string
var domainNames []apiGatewayTypes.DomainName
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(
context.TODO(),
&apigateway.GetDomainNamesInput{
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return domainNames, err
}
domainNames = append(domainNames, GetDomainNames.Items...)
//pagination
if GetDomainNames.Position == nil {
break
}
PaginationControl = GetDomainNames.Position
}
internal.Cache.Set(cacheKey, domainNames, cache.DefaultExpiration)
return domainNames, nil
}
// create a CachedApiGatewayGetBasePathMappings function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetBasePathMappings(client APIGatewayClientInterface, accountID string, region string, domain *string) ([]apiGatewayTypes.BasePathMapping, error) {
var PaginationControl *string
var basePathMappings []apiGatewayTypes.BasePathMapping
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(
context.TODO(),
&apigateway.GetBasePathMappingsInput{
DomainName: domain,
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return basePathMappings, err
}
basePathMappings = append(basePathMappings, GetBasePathMappings.Items...)
//pagination
if GetBasePathMappings.Position == nil {
break
}
PaginationControl = GetBasePathMappings.Position
}
internal.Cache.Set(cacheKey, basePathMappings, cache.DefaultExpiration)
return basePathMappings, nil
}
// create a CachedApiGatewayGetMethod function that accepts a client, account id, region, rest api id, and resource id. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetMethod(client APIGatewayClientInterface, accountID string, region string, restAPIID string, resourceID string, method string) (*apigateway.GetMethodOutput, error) {
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(),
&apigateway.GetMethodInput{
RestApiId: &restAPIID,
ResourceId: &resourceID,
HttpMethod: &method,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return &apigateway.GetMethodOutput{}, err
}
// Convert GetMethodOutput to CachedGetMethodOutput before caching
cachedVersion := toCachedGetMethodOutput(GetMethod)
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
return GetMethod, nil
}
// Convert from AWS SDK type to custom type for caching
func toCachedGetMethodOutput(gmo *apigateway.GetMethodOutput) *CachedGetMethodOutput {
return &CachedGetMethodOutput{
ApiKeyRequired: gmo.ApiKeyRequired,
AuthorizationScopes: gmo.AuthorizationScopes,
AuthorizationType: gmo.AuthorizationType,
AuthorizerId: gmo.AuthorizerId,
HttpMethod: gmo.HttpMethod,
MethodIntegration: gmo.MethodIntegration,
MethodResponses: gmo.MethodResponses,
OperationName: gmo.OperationName,
RequestModels: gmo.RequestModels,
RequestParameters: gmo.RequestParameters,
RequestValidatorId: gmo.RequestValidatorId,
}
}
// Convert back to AWS SDK type after fetching from cache
func fromCachedGetMethodOutput(cgmo *CachedGetMethodOutput) *apigateway.GetMethodOutput {
return &apigateway.GetMethodOutput{
ApiKeyRequired: cgmo.ApiKeyRequired,
AuthorizationScopes: cgmo.AuthorizationScopes,
AuthorizationType: cgmo.AuthorizationType,
AuthorizerId: cgmo.AuthorizerId,
HttpMethod: cgmo.HttpMethod,
MethodIntegration: cgmo.MethodIntegration,
MethodResponses: cgmo.MethodResponses,
OperationName: cgmo.OperationName,
RequestModels: cgmo.RequestModels,
RequestParameters: cgmo.RequestParameters,
RequestValidatorId: cgmo.RequestValidatorId,
// ResultMetadata: This will be missing or zero value; handle accordingly
}
}
// create a CachedApiGatewayGetUsagePlans function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetUsagePlans(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.UsagePlan, error) {
var PaginationControl *string
var usagePlans []apiGatewayTypes.UsagePlan
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(
context.TODO(),
&apigateway.GetUsagePlansInput{
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return usagePlans, err
}
usagePlans = append(usagePlans, GetUsagePlans.Items...)
//pagination
if GetUsagePlans.Position == nil {
break
}
PaginationControl = GetUsagePlans.Position
}
internal.Cache.Set(cacheKey, usagePlans, cache.DefaultExpiration)
return usagePlans, nil
}
// create a CachedApiGatewayGetUsagePlanKeys function that accepts a client, account id, region, and usage plan id. Make sure it handles caching, the region option and pagination if needed
func CachedApiGatewayGetUsagePlanKeys(client APIGatewayClientInterface, accountID string, region string, usagePlanID string) ([]apiGatewayTypes.UsagePlanKey, error) {
var PaginationControl *string
var usagePlanKeys []apiGatewayTypes.UsagePlanKey
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(
context.TODO(),
&apigateway.GetUsagePlanKeysInput{
UsagePlanId: &usagePlanID,
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return usagePlanKeys, err
}
usagePlanKeys = append(usagePlanKeys, GetUsagePlanKeys.Items...)
//pagination
if GetUsagePlanKeys.Position == nil {
break
}
PaginationControl = GetUsagePlanKeys.Position
}
internal.Cache.Set(cacheKey, usagePlanKeys, cache.DefaultExpiration)
return usagePlanKeys, nil
}
-143
View File
@@ -1,143 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apigateway"
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
)
type MockedAWSAPIGatewayClient struct {
}
func (m *MockedAWSAPIGatewayClient) GetRestApis(ctx context.Context, input *apigateway.GetRestApisInput, options ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error) {
return &apigateway.GetRestApisOutput{
Items: []apiGatewayTypes.RestApi{
{
Id: aws.String("abcdefg"),
Name: aws.String("api1"),
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
Types: []apiGatewayTypes.EndpointType{
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"),
Name: aws.String("api2"),
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
Types: []apiGatewayTypes.EndpointType{
apiGatewayTypes.EndpointTypeRegional,
},
},
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetStages(ctx context.Context, input *apigateway.GetStagesInput, options ...func(*apigateway.Options)) (*apigateway.GetStagesOutput, error) {
return &apigateway.GetStagesOutput{
Item: []apiGatewayTypes.Stage{
{
StageName: aws.String("stage1"),
},
{
StageName: aws.String("stage2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetResources(ctx context.Context, input *apigateway.GetResourcesInput, options ...func(*apigateway.Options)) (*apigateway.GetResourcesOutput, error) {
return &apigateway.GetResourcesOutput{
Items: []apiGatewayTypes.Resource{
{
Id: aws.String("resource1"),
Path: aws.String("/path1"),
ResourceMethods: map[string]apiGatewayTypes.Method{
"GET": {
ApiKeyRequired: aws.Bool(true),
AuthorizerId: aws.String("authorizer1"),
OperationName: aws.String("operation1"),
},
},
},
{
Id: aws.String("resource2"),
Path: aws.String("/path2"),
ResourceMethods: map[string]apiGatewayTypes.Method{
"ANY": {
ApiKeyRequired: aws.Bool(true),
AuthorizerId: aws.String("authorizer2"),
OperationName: aws.String("operation2"),
},
},
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetDomainNames(ctx context.Context, input *apigateway.GetDomainNamesInput, options ...func(*apigateway.Options)) (*apigateway.GetDomainNamesOutput, error) {
return &apigateway.GetDomainNamesOutput{
Items: []apiGatewayTypes.DomainName{},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetBasePathMappings(ctx context.Context, input *apigateway.GetBasePathMappingsInput, options ...func(*apigateway.Options)) (*apigateway.GetBasePathMappingsOutput, error) {
return &apigateway.GetBasePathMappingsOutput{
Items: []apiGatewayTypes.BasePathMapping{
{
BasePath: aws.String("basepath1"),
RestApiId: aws.String("abcdefg"),
Stage: aws.String("stage1"),
},
{
BasePath: aws.String("basepath2"),
RestApiId: aws.String("qwerty"),
Stage: aws.String("stage2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetMethod(ctx context.Context, input *apigateway.GetMethodInput, options ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error) {
return &apigateway.GetMethodOutput{
ApiKeyRequired: aws.Bool(true),
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetUsagePlans(ctx context.Context, input *apigateway.GetUsagePlansInput, options ...func(*apigateway.Options)) (*apigateway.GetUsagePlansOutput, error) {
return &apigateway.GetUsagePlansOutput{
Items: []apiGatewayTypes.UsagePlan{
{
Id: aws.String("usageplan1"),
Name: aws.String("usageplan1"),
ApiStages: []apiGatewayTypes.ApiStage{
{
ApiId: aws.String("abcdefg"),
Stage: aws.String("stage2"),
},
},
},
},
}, nil
}
func (m *MockedAWSAPIGatewayClient) GetUsagePlanKeys(ctx context.Context, input *apigateway.GetUsagePlanKeysInput, options ...func(*apigateway.Options)) (*apigateway.GetUsagePlanKeysOutput, error) {
return &apigateway.GetUsagePlanKeysOutput{
Items: []apiGatewayTypes.UsagePlanKey{
{
Id: aws.String("usageplankey1"),
Type: aws.String("API_KEY"),
Value: aws.String("23oieuwefo3rfs"),
},
{
Id: aws.String("usageplankey2"),
Type: aws.String("API_KEY"),
Value: aws.String("982yf98fdv8dlds"),
},
},
}, nil
}
+1 -225
View File
@@ -10,27 +10,14 @@ import (
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type APIGatewayv2ClientInterface interface {
GetApis(context.Context, *apigatewayv2.GetApisInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error)
GetDomainNames(context.Context, *apigatewayv2.GetDomainNamesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetDomainNamesOutput, error)
GetApiMappings(context.Context, *apigatewayv2.GetApiMappingsInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApiMappingsOutput, error)
GetStages(context.Context, *apigatewayv2.GetStagesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetStagesOutput, error)
GetRoutes(context.Context, *apigatewayv2.GetRoutesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetRoutesOutput, error)
}
type domainName apiGatwayV2Types.DomainName
type stage apiGatwayV2Types.Stage
func init() {
func RegisterApiGatewayV2Types() {
gob.Register([]apiGatwayV2Types.Api{})
//need to do this to avoid conflicts with the Instance type in the ec2 package
gob.Register([]domainName{})
gob.Register([]apiGatwayV2Types.ApiMapping{})
gob.Register([]stage{})
gob.Register([]apiGatwayV2Types.Route{})
}
func CachedAPIGatewayv2GetAPIs(client APIGatewayv2ClientInterface, accountID string, region string) ([]apiGatwayV2Types.Api, error) {
@@ -39,20 +26,8 @@ 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(),
@@ -80,202 +55,3 @@ func CachedAPIGatewayv2GetAPIs(client APIGatewayv2ClientInterface, accountID str
internal.Cache.Set(cacheKey, apis, cache.DefaultExpiration)
return apis, nil
}
func CachedAPIGatewayv2GetDomainNames(client APIGatewayv2ClientInterface, accountID string, region string) ([]domainName, error) {
var PaginationControl *string
var domainNames []domainName
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(),
&apigatewayv2.GetDomainNamesInput{
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return domainNames, err
}
// convert []apiGatwayV2Types.DomainName to []domainName
// Convert each element to domainName before appending to avoid conflicting with apigateway's domainname type
for _, item := range GetDomainNames.Items {
domainNames = append(domainNames, domainName(item))
}
//pagination
if GetDomainNames.NextToken == nil {
break
}
PaginationControl = GetDomainNames.NextToken
}
internal.Cache.Set(cacheKey, domainNames, cache.DefaultExpiration)
return domainNames, nil
}
func CachedAPIGatewayv2GetApiMappings(client APIGatewayv2ClientInterface, accountID string, region string, domain string) ([]apiGatwayV2Types.ApiMapping, error) {
var PaginationControl *string
var apiMappings []apiGatwayV2Types.ApiMapping
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(),
&apigatewayv2.GetApiMappingsInput{
DomainName: &domain,
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return apiMappings, err
}
apiMappings = append(apiMappings, GetApiMappings.Items...)
//pagination
if GetApiMappings.NextToken == nil {
break
}
PaginationControl = GetApiMappings.NextToken
}
internal.Cache.Set(cacheKey, apiMappings, cache.DefaultExpiration)
return apiMappings, nil
}
func CachedAPIGatewayv2GetStages(client APIGatewayv2ClientInterface, accountID string, region string, apiID string) ([]apiGatwayV2Types.Stage, error) {
var PaginationControl *string
var stages []apiGatwayV2Types.Stage
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(),
&apigatewayv2.GetStagesInput{
ApiId: &apiID,
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return stages, err
}
stages = append(stages, GetStages.Items...)
//pagination
if GetStages.NextToken == nil {
break
}
PaginationControl = GetStages.NextToken
}
internal.Cache.Set(cacheKey, stages, cache.DefaultExpiration)
return stages, nil
}
func CachedAPIGatewayv2GetRoutes(client APIGatewayv2ClientInterface, accountID string, region string, apiID string) ([]apiGatwayV2Types.Route, error) {
var PaginationControl *string
var routes []apiGatwayV2Types.Route
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(),
&apigatewayv2.GetRoutesInput{
ApiId: &apiID,
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return routes, err
}
routes = append(routes, GetRoutes.Items...)
//pagination
if GetRoutes.NextToken == nil {
break
}
PaginationControl = GetRoutes.NextToken
}
internal.Cache.Set(cacheKey, routes, cache.DefaultExpiration)
return routes, nil
}
-83
View File
@@ -1,83 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
)
type MockedAWSAPIGatewayv2Client struct {
}
func (m *MockedAWSAPIGatewayv2Client) GetApis(ctx context.Context, input *apigatewayv2.GetApisInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error) {
return &apigatewayv2.GetApisOutput{
Items: []apiGatwayV2Types.Api{
{
ApiId: aws.String("asdfsdfasdf"),
Name: aws.String("api1"),
ApiEndpoint: aws.String("https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com"),
},
{
ApiId: aws.String("qwertyqwerty"),
Name: aws.String("api2"),
ApiEndpoint: aws.String("https://qwertyqwerty.execute-api.us-east-1.amazonaws.com"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayv2Client) GetDomainNames(ctx context.Context, input *apigatewayv2.GetDomainNamesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetDomainNamesOutput, error) {
return &apigatewayv2.GetDomainNamesOutput{
Items: []apiGatwayV2Types.DomainName{
{
DomainName: aws.String("domain1"),
},
{
DomainName: aws.String("domain2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayv2Client) GetApiMappings(ctx context.Context, input *apigatewayv2.GetApiMappingsInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApiMappingsOutput, error) {
return &apigatewayv2.GetApiMappingsOutput{
Items: []apiGatwayV2Types.ApiMapping{
{
ApiMappingId: aws.String("apiMapping1"),
},
{
ApiMappingId: aws.String("apiMapping2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayv2Client) GetStages(ctx context.Context, input *apigatewayv2.GetStagesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetStagesOutput, error) {
return &apigatewayv2.GetStagesOutput{
Items: []apiGatwayV2Types.Stage{
{
StageName: aws.String("stage1"),
},
{
StageName: aws.String("stage2"),
},
},
}, nil
}
func (m *MockedAWSAPIGatewayv2Client) GetRoutes(ctx context.Context, input *apigatewayv2.GetRoutesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetRoutesOutput, error) {
return &apigatewayv2.GetRoutesOutput{
Items: []apiGatwayV2Types.Route{
{
RouteId: aws.String("route1"),
RouteKey: aws.String("POST /route1"),
},
{
RouteId: aws.String("route2"),
RouteKey: aws.String("GET /route2"),
},
},
}, nil
}
+1 -14
View File
@@ -9,14 +9,13 @@ 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 {
ListServices(context.Context, *apprunner.ListServicesInput, ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error)
}
func init() {
func RegisterApprunnerTypes() {
gob.Register([]apprunnerTypes.Service{})
gob.Register([]apprunnerTypes.ServiceSummary{})
}
@@ -27,20 +26,8 @@ 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(),
-25
View File
@@ -1,25 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apprunner"
apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
)
type MockedAppRunnerClient struct {
}
func (m *MockedAppRunnerClient) ListServices(ctx context.Context, input *apprunner.ListServicesInput, options ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error) {
return &apprunner.ListServicesOutput{
ServiceSummaryList: []apprunnerTypes.ServiceSummary{
{
ServiceName: aws.String("service1"),
},
{
ServiceName: aws.String("service2"),
},
},
}, nil
}
-123
View File
@@ -1,123 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"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 {
ListDataCatalogs(context.Context, *athena.ListDataCatalogsInput, ...func(*athena.Options)) (*athena.ListDataCatalogsOutput, error)
ListDatabases(context.Context, *athena.ListDatabasesInput, ...func(*athena.Options)) (*athena.ListDatabasesOutput, error)
}
func init() {
gob.Register([]athenaTypes.DataCatalogSummary{})
}
func CachedAthenaListDataCatalogs(client AWSAthenaClientInterface, accountID string, region string) ([]athenaTypes.DataCatalogSummary, error) {
var PaginationControl *string
var dataCatalogs []athenaTypes.DataCatalogSummary
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(),
&athena.ListDataCatalogsInput{
NextToken: PaginationControl,
},
func(o *athena.Options) {
o.Region = region
},
)
if err != nil {
return dataCatalogs, err
}
dataCatalogs = append(dataCatalogs, ListDataCatalogs.DataCatalogsSummary...)
//pagination
if ListDataCatalogs.NextToken == nil {
break
}
PaginationControl = ListDataCatalogs.NextToken
}
internal.Cache.Set(cacheKey, dataCatalogs, cache.DefaultExpiration)
return dataCatalogs, nil
}
func CachedAthenaListDatabases(client AWSAthenaClientInterface, accountID string, region string, catalogName string) ([]string, error) {
var PaginationControl *string
var databases []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(),
&athena.ListDatabasesInput{
CatalogName: &catalogName,
NextToken: PaginationControl,
},
func(o *athena.Options) {
o.Region = region
},
)
if err != nil {
return databases, err
}
for _, database := range ListDatabases.DatabaseList {
databases = append(databases, *database.Name)
}
//pagination
if ListDatabases.NextToken == nil {
break
}
PaginationControl = ListDatabases.NextToken
}
internal.Cache.Set(cacheKey, databases, cache.DefaultExpiration)
return databases, nil
}
-38
View File
@@ -1,38 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/athena"
athenaTypes "github.com/aws/aws-sdk-go-v2/service/athena/types"
)
type MockedAWSAthenaClient struct {
}
func (m *MockedAWSAthenaClient) ListDatabases(ctx context.Context, input *athena.ListDatabasesInput, options ...func(*athena.Options)) (*athena.ListDatabasesOutput, error) {
return &athena.ListDatabasesOutput{
DatabaseList: []athenaTypes.Database{
{
Name: aws.String("db1"),
},
{
Name: aws.String("db2"),
},
},
}, nil
}
func (m *MockedAWSAthenaClient) ListDataCatalogs(ctx context.Context, input *athena.ListDataCatalogsInput, options ...func(*athena.Options)) (*athena.ListDataCatalogsOutput, error) {
return &athena.ListDataCatalogsOutput{
DataCatalogsSummary: []athenaTypes.DataCatalogSummary{
{
CatalogName: aws.String("catalog1"),
},
{
CatalogName: aws.String("catalog2"),
},
},
}, nil
}
-111
View File
@@ -1,111 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"github.com/BishopFox/cloudfox/internal"
"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 {
ListEnvironments(context.Context, *cloud9.ListEnvironmentsInput, ...func(*cloud9.Options)) (*cloud9.ListEnvironmentsOutput, error)
DescribeEnvironments(context.Context, *cloud9.DescribeEnvironmentsInput, ...func(*cloud9.Options)) (*cloud9.DescribeEnvironmentsOutput, error)
}
func init() {
gob.Register([]string{})
}
func CachedCloud9ListEnvironments(client AWSCloud9ClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var environments []string
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(),
&cloud9.ListEnvironmentsInput{
NextToken: PaginationControl,
},
func(o *cloud9.Options) {
o.Region = region
},
)
if err != nil {
return environments, err
}
environments = append(environments, ListEnvironments.EnvironmentIds...)
//pagination
if ListEnvironments.NextToken == nil {
break
}
PaginationControl = ListEnvironments.NextToken
}
internal.Cache.Set(cacheKey, environments, cache.DefaultExpiration)
return environments, nil
}
func CachedCloud9DescribeEnvironments(client AWSCloud9ClientInterface, accountID string, region string, environmentIDs []string) ([]cloud9Types.Environment, error) {
var environments []cloud9Types.Environment
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(),
&cloud9.DescribeEnvironmentsInput{
EnvironmentIds: []string{environmentID},
},
func(o *cloud9.Options) {
o.Region = region
},
)
if err != nil {
return environments, err
}
environments = append(environments, DescribeEnvironments.Environments...)
}
internal.Cache.Set(cacheKey, environments, cache.DefaultExpiration)
return environments, nil
}
-36
View File
@@ -1,36 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloud9"
"github.com/aws/aws-sdk-go-v2/service/cloud9/types"
)
type MockedAWSCloud9Client struct {
}
func (m *MockedAWSCloud9Client) ListEnvironments(ctx context.Context, input *cloud9.ListEnvironmentsInput, options ...func(*cloud9.Options)) (*cloud9.ListEnvironmentsOutput, error) {
return &cloud9.ListEnvironmentsOutput{
EnvironmentIds: []string{
"env1",
"env2",
},
}, nil
}
func (m *MockedAWSCloud9Client) DescribeEnvironments(ctx context.Context, input *cloud9.DescribeEnvironmentsInput, options ...func(*cloud9.Options)) (*cloud9.DescribeEnvironmentsOutput, error) {
return &cloud9.DescribeEnvironmentsOutput{
Environments: []types.Environment{
{
Name: aws.String("env1"),
Arn: aws.String("arn:aws:cloud9:us-east-1:123456789012:environment/env1"),
},
{
Name: aws.String("env2"),
Arn: aws.String("arn:aws:cloud9:us-east-1:123456789012:environment/env2"),
},
},
}, nil
}
+1 -38
View File
@@ -10,7 +10,6 @@ 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 {
@@ -19,7 +18,7 @@ type CloudFormationClientInterface interface {
ListStacks(context.Context, *cloudformation.ListStacksInput, ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error)
}
func init() {
func RegisterCloudFormationTypes() {
gob.Register([]cloudFormationTypes.Stack{})
gob.Register([]cloudFormationTypes.StackSummary{})
}
@@ -30,20 +29,8 @@ 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(),
@@ -76,20 +63,8 @@ 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(),
@@ -114,20 +89,8 @@ 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(),
-73
View File
@@ -1,73 +0,0 @@
package sdk
import (
"context"
"encoding/json"
"log"
"time"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
)
const DESCRIBE_STACKS_TEST_FILE = "./test-data/cloudformation-describestacks.json"
const TEMPLATE_BODY_TEST_FILE = "./test-data/cloudformation-getTemplate.json"
type Stacks struct {
Stacks []struct {
StackID string `json:"StackId"`
Description string `json:"Description"`
Tags []interface{} `json:"Tags"`
Outputs []types.Output `json:"Outputs"`
Parameters []types.Parameter `json:"Parameters"`
StackStatusReason interface{} `json:"StackStatusReason"`
CreationTime time.Time `json:"CreationTime"`
Capabilities []interface{} `json:"Capabilities"`
StackName string `json:"StackName"`
RoleArn string `json:"RoleArn"`
StackStatus string `json:"StackStatus"`
DisableRollback bool `json:"DisableRollback"`
} `json:"Stacks"`
}
type TemplateBody struct {
TemplateBody string `json:"TemplateBody"`
}
type MockedCloudformationClient struct {
describeStacks Stacks
getTemplateBody TemplateBody
}
func (m *MockedCloudformationClient) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.describeStacks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_STACKS_TEST_FILE)
}
var stacks []types.Stack
for _, stack := range m.describeStacks.Stacks {
stacks = append(stacks, types.Stack{
StackName: &stack.StackName,
RoleARN: &stack.RoleArn,
Outputs: stack.Outputs,
Parameters: stack.Parameters,
})
}
return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil
}
func (m *MockedCloudformationClient) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.getTemplateBody)
if err != nil {
log.Fatalf("can't unmarshall file %s", TEMPLATE_BODY_TEST_FILE)
}
return &cloudformation.GetTemplateOutput{TemplateBody: &m.getTemplateBody.TemplateBody}, nil
}
func (m *MockedCloudformationClient) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) {
return &cloudformation.ListStacksOutput{}, nil
}
-67
View File
@@ -1,67 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"github.com/BishopFox/cloudfox/internal"
"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 {
ListDistributions(ctx context.Context, params *cloudfront.ListDistributionsInput, optFns ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error)
}
func init() {
gob.Register([]cloudfrontTypes.DistributionSummary{})
gob.Register(cloudfrontTypes.DistributionSummary{})
}
func CachedCloudFrontListDistributions(CloudFrontClient AWSCloudFrontClientInterface, accountID string) ([]cloudfrontTypes.DistributionSummary, error) {
var PaginationControl *string
var distributions []cloudfrontTypes.DistributionSummary
cacheKey := "cloudfront-ListDistributions-" + accountID
cached, found := internal.Cache.Get(cacheKey)
if found {
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(
context.TODO(),
&cloudfront.ListDistributionsInput{
Marker: PaginationControl,
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
distributions = append(distributions, ListDistributions.DistributionList.Items...)
// Pagination control.
if ListDistributions.DistributionList.NextMarker != nil {
PaginationControl = ListDistributions.DistributionList.NextMarker
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, distributions, cache.DefaultExpiration)
return distributions, nil
}
-27
View File
@@ -1,27 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudfront"
cloudfrontTypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types"
)
type MockedAWSCloudFrontClient struct {
}
func (m *MockedAWSCloudFrontClient) ListDistributions(ctx context.Context, input *cloudfront.ListDistributionsInput, options ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error) {
return &cloudfront.ListDistributionsOutput{
DistributionList: &cloudfrontTypes.DistributionList{
Items: []cloudfrontTypes.DistributionSummary{
{
Id: aws.String("distribution1"),
},
{
Id: aws.String("distribution2"),
},
},
},
}, nil
}
-119
View File
@@ -1,119 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"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 {
ListDomains(context.Context, *codeartifact.ListDomainsInput, ...func(*codeartifact.Options)) (*codeartifact.ListDomainsOutput, error)
ListRepositories(context.Context, *codeartifact.ListRepositoriesInput, ...func(*codeartifact.Options)) (*codeartifact.ListRepositoriesOutput, error)
}
func init() {
gob.Register([]codeArtifactTypes.DomainSummary{})
gob.Register([]codeArtifactTypes.RepositorySummary{})
}
func CachedCodeArtifactListDomains(client AWSCodeArtifactClientInterface, accountID string, region string) ([]codeArtifactTypes.DomainSummary, error) {
var PaginationControl *string
var domains []codeArtifactTypes.DomainSummary
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(),
&codeartifact.ListDomainsInput{
NextToken: PaginationControl,
},
func(o *codeartifact.Options) {
o.Region = region
},
)
if err != nil {
return domains, err
}
domains = append(domains, ListDomains.Domains...)
//pagination
if ListDomains.NextToken == nil {
break
}
PaginationControl = ListDomains.NextToken
}
internal.Cache.Set(cacheKey, domains, cache.DefaultExpiration)
return domains, nil
}
func CachedCodeArtifactListRepositories(client AWSCodeArtifactClientInterface, accountID string, region string) ([]codeArtifactTypes.RepositorySummary, error) {
var PaginationControl *string
var repositories []codeArtifactTypes.RepositorySummary
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(),
&codeartifact.ListRepositoriesInput{
NextToken: PaginationControl,
},
func(o *codeartifact.Options) {
o.Region = region
},
)
if err != nil {
return repositories, err
}
repositories = append(repositories, ListRepositories.Repositories...)
//pagination
if ListRepositories.NextToken == nil {
break
}
PaginationControl = ListRepositories.NextToken
}
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
return repositories, nil
}
-46
View File
@@ -1,46 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codeartifact"
codeArtifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types"
)
type MockedAWSCodeArtifactClient struct {
}
func (m *MockedAWSCodeArtifactClient) ListRepositories(ctx context.Context, input *codeartifact.ListRepositoriesInput, options ...func(*codeartifact.Options)) (*codeartifact.ListRepositoriesOutput, error) {
return &codeartifact.ListRepositoriesOutput{
NextToken: nil,
Repositories: []codeArtifactTypes.RepositorySummary{
{
Name: aws.String("repo1"),
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:repository/repo1"),
DomainName: aws.String("domain1"),
},
{
Name: aws.String("repo2"),
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:repository/repo2"),
DomainName: aws.String("domain1"),
},
},
}, nil
}
func (m *MockedAWSCodeArtifactClient) ListDomains(ctx context.Context, input *codeartifact.ListDomainsInput, options ...func(*codeartifact.Options)) (*codeartifact.ListDomainsOutput, error) {
return &codeartifact.ListDomainsOutput{
NextToken: nil,
Domains: []codeArtifactTypes.DomainSummary{
{
Name: aws.String("domain1"),
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:domain/domain1"),
},
{
Name: aws.String("domain2"),
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:domain/domain2"),
},
},
}, nil
}
+2 -40
View File
@@ -10,7 +10,6 @@ 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 {
@@ -19,9 +18,8 @@ type CodeBuildClientInterface interface {
GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error)
}
func init() {
func RegisterCodeBuildTypes() {
gob.Register(codeBuildTypes.Project{})
gob.Register([]codeBuildTypes.Project{})
}
@@ -32,20 +30,8 @@ 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(
@@ -84,20 +70,8 @@ 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(),
@@ -113,7 +87,7 @@ func CachedCodeBuildBatchGetProjects(CodeBuildClient CodeBuildClientInterface, a
return project, err
}
internal.Cache.Set(cacheKey, BatchGetProjects.Projects, cache.DefaultExpiration)
internal.Cache.Set(cacheKey, BatchGetProjects.Projects[0], cache.DefaultExpiration)
return BatchGetProjects.Projects[0], nil
}
@@ -122,20 +96,8 @@ 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(),
-53
View File
@@ -1,53 +0,0 @@
package sdk
import (
"context"
"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 {
}
func (m *MockedCodeBuildClient) ListProjects(ctx context.Context, input *codebuild.ListProjectsInput, options ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) {
return &codebuild.ListProjectsOutput{
Projects: []string{
"project1",
"project2",
},
}, 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(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "codebuild:BatchGetProjects",
"Resource": "*",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
}
]
}
`),
}, nil
}
-69
View File
@@ -1,69 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"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 {
ListRepositories(context.Context, *codecommit.ListRepositoriesInput, ...func(*codecommit.Options)) (*codecommit.ListRepositoriesOutput, error)
}
func init() {
gob.Register([]codeCommitTypes.RepositoryNameIdPair{})
}
func CachedCodeCommitListRepositories(client AWSCodeCommitClientInterface, accountID string, region string) ([]codeCommitTypes.RepositoryNameIdPair, error) {
var PaginationControl *string
var repositories []codeCommitTypes.RepositoryNameIdPair
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(),
&codecommit.ListRepositoriesInput{
NextToken: PaginationControl,
},
func(o *codecommit.Options) {
o.Region = region
},
)
if err != nil {
return repositories, err
}
repositories = append(repositories, ListRepositories.Repositories...)
//pagination
if ListRepositories.NextToken == nil {
break
}
PaginationControl = ListRepositories.NextToken
}
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
return repositories, nil
}
-30
View File
@@ -1,30 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codecommit"
codeCommitTypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types"
)
// create mocks for codecommit ListRepositories
type MockedAWSCodeCommitClient struct {
}
func (m *MockedAWSCodeCommitClient) ListRepositories(ctx context.Context, input *codecommit.ListRepositoriesInput, options ...func(*codecommit.Options)) (*codecommit.ListRepositoriesOutput, error) {
return &codecommit.ListRepositoriesOutput{
NextToken: nil,
Repositories: []codeCommitTypes.RepositoryNameIdPair{
{
RepositoryId: aws.String("repo1"),
RepositoryName: aws.String("repo1"),
},
{
RepositoryId: aws.String("repo2"),
RepositoryName: aws.String("repo2"),
},
},
}, nil
}
-291
View File
@@ -1,291 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"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 {
ListApplications(context.Context, *codedeploy.ListApplicationsInput, ...func(*codedeploy.Options)) (*codedeploy.ListApplicationsOutput, error)
GetApplication(context.Context, *codedeploy.GetApplicationInput, ...func(*codedeploy.Options)) (*codedeploy.GetApplicationOutput, error)
ListDeployments(context.Context, *codedeploy.ListDeploymentsInput, ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentsOutput, error)
GetDeployment(context.Context, *codedeploy.GetDeploymentInput, ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentOutput, error)
ListDeploymentConfigs(context.Context, *codedeploy.ListDeploymentConfigsInput, ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentConfigsOutput, error)
GetDeploymentConfig(context.Context, *codedeploy.GetDeploymentConfigInput, ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentConfigOutput, error)
}
func init() {
gob.Register([]string{})
gob.Register([]codeDeployTypes.ApplicationInfo{})
gob.Register([]codeDeployTypes.DeploymentConfigInfo{})
gob.Register([]codeDeployTypes.DeploymentInfo{})
}
func CachedCodeDeployListApplications(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var applications []string
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(),
&codedeploy.ListApplicationsInput{
NextToken: PaginationControl,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return applications, err
}
applications = append(applications, ListApplications.Applications...)
//pagination
if ListApplications.NextToken == nil {
break
}
PaginationControl = ListApplications.NextToken
}
internal.Cache.Set(cacheKey, applications, cache.DefaultExpiration)
return applications, nil
}
func CachedCodeDeployListDeploymentConfigs(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var deploymentConfigs []string
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(),
&codedeploy.ListDeploymentConfigsInput{
NextToken: PaginationControl,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return deploymentConfigs, err
}
deploymentConfigs = append(deploymentConfigs, ListDeploymentConfigs.DeploymentConfigsList...)
//pagination
if ListDeploymentConfigs.NextToken == nil {
break
}
PaginationControl = ListDeploymentConfigs.NextToken
}
internal.Cache.Set(cacheKey, deploymentConfigs, cache.DefaultExpiration)
return deploymentConfigs, nil
}
func CachedCodeDeployListDeployments(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var deployments []string
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(),
&codedeploy.ListDeploymentsInput{
NextToken: PaginationControl,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return deployments, err
}
deployments = append(deployments, ListDeployments.Deployments...)
//pagination
if ListDeployments.NextToken == nil {
break
}
PaginationControl = ListDeployments.NextToken
}
internal.Cache.Set(cacheKey, deployments, cache.DefaultExpiration)
return deployments, nil
}
func CachedCodeDeployGetApplication(client AWSCodeDeployClientInterface, accountID string, region string, applicationName string) (codeDeployTypes.ApplicationInfo, error) {
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{
ApplicationName: &applicationName,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return *GetApplication.Application, err
}
internal.Cache.Set(cacheKey, *GetApplication.Application, cache.DefaultExpiration)
return *GetApplication.Application, nil
}
func CachedCodeDeployGetDeploymentConfig(client AWSCodeDeployClientInterface, accountID string, region string, deploymentConfigName string) (codeDeployTypes.DeploymentConfigInfo, error) {
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{
DeploymentConfigName: &deploymentConfigName,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return *GetDeploymentConfig.DeploymentConfigInfo, err
}
internal.Cache.Set(cacheKey, *GetDeploymentConfig.DeploymentConfigInfo, cache.DefaultExpiration)
return *GetDeploymentConfig.DeploymentConfigInfo, nil
}
func CachedCodeDeployGetDeployment(client AWSCodeDeployClientInterface, accountID string, region string, deploymentID string) (codeDeployTypes.DeploymentInfo, error) {
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{
DeploymentId: &deploymentID,
},
func(o *codedeploy.Options) {
o.Region = region
},
)
if err != nil {
return *GetDeployment.DeploymentInfo, err
}
internal.Cache.Set(cacheKey, *GetDeployment.DeploymentInfo, cache.DefaultExpiration)
return *GetDeployment.DeploymentInfo, nil
}
-85
View File
@@ -1,85 +0,0 @@
package sdk
import (
"context"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codedeploy"
codedeployTypes "github.com/aws/aws-sdk-go-v2/service/codedeploy/types"
)
type MockedCodedeployClient struct {
}
func (m *MockedCodedeployClient) ListApplications(ctx context.Context, input *codedeploy.ListApplicationsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListApplicationsOutput, error) {
return &codedeploy.ListApplicationsOutput{
Applications: []string{
"app1",
"app2",
},
}, nil
}
func (m *MockedCodedeployClient) ListDeployments(ctx context.Context, input *codedeploy.ListDeploymentsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentsOutput, error) {
return &codedeploy.ListDeploymentsOutput{
Deployments: []string{
"deployment1",
"deployment2",
},
}, nil
}
func (m *MockedCodedeployClient) ListDeploymentConfigs(ctx context.Context, input *codedeploy.ListDeploymentConfigsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentConfigsOutput, error) {
return &codedeploy.ListDeploymentConfigsOutput{
DeploymentConfigsList: []string{
"deploymentConfig1",
"deploymentConfig2",
},
}, nil
}
func (m *MockedCodedeployClient) GetApplication(ctx context.Context, input *codedeploy.GetApplicationInput, options ...func(*codedeploy.Options)) (*codedeploy.GetApplicationOutput, error) {
return &codedeploy.GetApplicationOutput{
Application: &codedeployTypes.ApplicationInfo{
ApplicationName: aws.String("application1"),
ApplicationId: aws.String("application1"),
CreateTime: aws.Time(time.Now()),
GitHubAccountName: aws.String("github"),
LinkedToGitHub: true,
},
}, nil
}
func (m *MockedCodedeployClient) GetDeployment(ctx context.Context, input *codedeploy.GetDeploymentInput, options ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentOutput, error) {
return &codedeploy.GetDeploymentOutput{
DeploymentInfo: &codedeployTypes.DeploymentInfo{
ApplicationName: aws.String("application1"),
CreateTime: aws.Time(time.Now()),
DeploymentId: aws.String("deployment1"),
DeploymentOverview: &codedeployTypes.DeploymentOverview{
Failed: 0,
InProgress: 0,
Pending: 0,
Ready: 0,
Skipped: 0,
Succeeded: 0,
},
DeploymentConfigName: aws.String("deploymentConfig1"),
Status: codedeployTypes.DeploymentStatusSucceeded,
},
}, nil
}
func (m *MockedCodedeployClient) GetDeploymentConfig(ctx context.Context, input *codedeploy.GetDeploymentConfigInput, options ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentConfigOutput, error) {
return &codedeploy.GetDeploymentConfigOutput{
DeploymentConfigInfo: &codedeployTypes.DeploymentConfigInfo{
DeploymentConfigName: aws.String("deploymentConfig1"),
ComputePlatform: codedeployTypes.ComputePlatformServer,
MinimumHealthyHosts: &codedeployTypes.MinimumHealthyHosts{
Type: codedeployTypes.MinimumHealthyHostsTypeFleetPercent,
Value: 100,
},
},
}, nil
}
-69
View File
@@ -1,69 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"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 {
ListPipelines(ctx context.Context, input *datapipeline.ListPipelinesInput, opts ...func(*datapipeline.Options)) (*datapipeline.ListPipelinesOutput, error)
}
func init() {
gob.Register([]dataPipelineTypes.PipelineIdName{})
}
func CachedDataPipelineListPipelines(client AWSDataPipelineClientInterface, accountID string, region string) ([]dataPipelineTypes.PipelineIdName, error) {
var PaginationControl *string
var pipelines []dataPipelineTypes.PipelineIdName
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(),
&datapipeline.ListPipelinesInput{
Marker: PaginationControl,
},
func(o *datapipeline.Options) {
o.Region = region
},
)
if err != nil {
return pipelines, err
}
pipelines = append(pipelines, ListPipelines.PipelineIdList...)
//pagination
if ListPipelines.Marker == nil {
break
}
PaginationControl = ListPipelines.Marker
}
internal.Cache.Set(cacheKey, pipelines, cache.DefaultExpiration)
return pipelines, nil
}
-27
View File
@@ -1,27 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/datapipeline"
dataPipelineTypes "github.com/aws/aws-sdk-go-v2/service/datapipeline/types"
)
type MockedDataPipelineClient struct {
}
func (m *MockedDataPipelineClient) ListPipelines(ctx context.Context, input *datapipeline.ListPipelinesInput, options ...func(*datapipeline.Options)) (*datapipeline.ListPipelinesOutput, error) {
return &datapipeline.ListPipelinesOutput{
PipelineIdList: []dataPipelineTypes.PipelineIdName{
{
Id: aws.String("pipeline1"),
Name: aws.String("pipeline1"),
},
{
Id: aws.String("pipeline2"),
Name: aws.String("pipeline2"),
},
},
}, nil
}
+3 -39
View File
@@ -9,8 +9,8 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/docdb"
docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
type DocDBClientInterface interface {
@@ -19,9 +19,9 @@ type DocDBClientInterface interface {
DescribeDBInstances(context.Context, *docdb.DescribeDBInstancesInput, ...func(*docdb.Options)) (*docdb.DescribeDBInstancesOutput, error)
}
func init() {
func RegisterDocDBTypes() {
gob.Register([]docdbTypes.GlobalCluster{})
//gob.Register([]docdbTypes.DBCluster{})
gob.Register([]docdbTypes.DBCluster{})
//gob.Register([]docdbTypes.DBInstance{})
}
@@ -32,20 +32,8 @@ 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(),
@@ -80,20 +68,8 @@ 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(),
@@ -134,20 +110,8 @@ 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(),
-25
View File
@@ -1,25 +0,0 @@
package sdk
import (
"context"
"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"
)
type MockedAWSDocsDBClient struct {
}
func (m *MockedAWSDocsDBClient) DescribeDBClusters(ctx context.Context, input *docdb.DescribeDBClustersInput, options ...func(*docdb.Options)) (*docdb.DescribeDBClustersOutput, error) {
return &docdb.DescribeDBClustersOutput{
DBClusters: []docdbTypes.DBCluster{
{
DBClusterIdentifier: aws.String("cluster1"),
},
{
DBClusterIdentifier: aws.String("cluster2"),
},
},
}, nil
}
-122
View File
@@ -1,122 +0,0 @@
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
}
+1 -28
View File
@@ -9,7 +9,6 @@ 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 {
@@ -17,7 +16,7 @@ type DynamoDBClientInterface interface {
DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error)
}
func init() {
func RegisterDynamoDBTypes() {
gob.Register([]string{})
gob.Register(dynamoDBTypes.TableDescription{})
}
@@ -28,21 +27,8 @@ 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(),
@@ -76,22 +62,9 @@ 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{
-25
View File
@@ -1,25 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type MockedAWSDynamoDBClient struct {
}
func (m *MockedAWSDynamoDBClient) DescribeTable(ctx context.Context, input *dynamodb.DescribeTableInput, options ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
return &dynamodb.DescribeTableOutput{
Table: &dynamodbTypes.TableDescription{
TableName: input.TableName,
},
}, nil
}
func (m *MockedAWSDynamoDBClient) ListTables(ctx context.Context, input *dynamodb.ListTablesInput, options ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) {
return &dynamodb.ListTablesOutput{
TableNames: []string{"table1", "table2"},
}, nil
}
+3 -292
View File
@@ -6,52 +6,27 @@ import (
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"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 {
type EC2ClientInterface interface {
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
DescribeNetworkInterfaces(context.Context, *ec2.DescribeNetworkInterfacesInput, ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error)
DescribeSnapshots(context.Context, *ec2.DescribeSnapshotsInput, ...func(*ec2.Options)) (*ec2.DescribeSnapshotsOutput, error)
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() {
func RegisterEC2Types() {
gob.Register([]ec2Types.Instance{})
gob.Register([]ec2Types.NetworkInterface{})
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) {
func CachedEC2DescribeInstances(client EC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) {
var PaginationControl *string
var instances []ec2Types.Instance
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(),
@@ -81,267 +56,3 @@ func CachedEC2DescribeInstances(client AWSEC2ClientInterface, accountID string,
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
return instances, nil
}
func CachedEC2DescribeInstanceAttributeUserData(client AWSEC2ClientInterface, accountID string, region string, instanceID string) (string, error) {
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{
Attribute: ec2Types.InstanceAttributeNameUserData,
InstanceId: &instanceID,
},
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return "", err
}
internal.Cache.Set(cacheKey, aws.ToString(DescribeInstanceAttribute.UserData.Value), cache.DefaultExpiration)
return aws.ToString(DescribeInstanceAttribute.UserData.Value), nil
}
func CachedEC2DescribeNetworkInterfaces(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.NetworkInterface, error) {
var PaginationControl *string
var NetworkInterfaces []ec2Types.NetworkInterface
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(),
&(ec2.DescribeNetworkInterfacesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return NetworkInterfaces, err
}
NetworkInterfaces = append(NetworkInterfaces, DescribeNetworkInterfaces.NetworkInterfaces...)
if DescribeNetworkInterfaces.NextToken == nil {
break
}
PaginationControl = DescribeNetworkInterfaces.NextToken
}
internal.Cache.Set(cacheKey, NetworkInterfaces, cache.DefaultExpiration)
return NetworkInterfaces, nil
}
func CachedEC2DescribeSnapshots(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Snapshot, error) {
var PaginationControl *string
var Snapshots []ec2Types.Snapshot
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(),
&(ec2.DescribeSnapshotsInput{
NextToken: PaginationControl,
OwnerIds: []string{accountID},
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return Snapshots, err
}
Snapshots = append(Snapshots, DescribeSnapshots.Snapshots...)
if DescribeSnapshots.NextToken == nil {
break
}
PaginationControl = DescribeSnapshots.NextToken
}
internal.Cache.Set(cacheKey, Snapshots, cache.DefaultExpiration)
return Snapshots, nil
}
func CachedEC2DescribeVolumes(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Volume, error) {
var PaginationControl *string
var Volumes []ec2Types.Volume
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(),
&(ec2.DescribeVolumesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return Volumes, err
}
Volumes = append(Volumes, DescribeVolumes.Volumes...)
if DescribeVolumes.NextToken == nil {
break
}
PaginationControl = DescribeVolumes.NextToken
}
internal.Cache.Set(cacheKey, Volumes, cache.DefaultExpiration)
return Volumes, nil
}
func CachedEC2DescribeImages(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Image, error) {
var PaginationControl *string
var Images []ec2Types.Image
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(),
&(ec2.DescribeImagesInput{
Owners: []string{accountID},
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return Images, err
}
Images = append(Images, DescribeImages.Images...)
if DescribeImages.NextToken == nil {
break
}
PaginationControl = DescribeImages.NextToken
}
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
}
-432
View File
@@ -1,432 +0,0 @@
package sdk
// create test for the Cached EC2 functions. Create a mocked client and mocked functions for each of the methods used in the ec2.go file
import (
"context"
"encoding/json"
"log"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
)
const DESCRIBE_NETWORK_INTEFACES_TEST_FILE = "./test-data/describe-network-interfaces.json"
const DESCRIBE_INSTANCES_TEST_FILE = "./test-data/ec2-describeInstances.json"
const DESCRIBE_SNAPSHOTS_TEST_FILE = "./test-data/ec2-describeSnapshots.json"
const DESCRIBE_VOLUMES_TEST_FILE = "./test-data/ec2-describeVolumes.json"
const DESCRIBE_IMAGES_TEST_FILE = "./test-data/ec2-describeImages.json"
type MockedEC2Client2 struct {
describeNetworkInterfaces DescribeNetworkInterfaces
describeInstances DescribeInstances
describeSnapshots DescribeSnapshots
describeVolumes DescribeVolumes
describeImages DescribeImages
describeVpcEndpoints DescribeVpcEndpoints
}
type DescribeNetworkInterfaces struct {
NetworkInterfaces []struct {
Status string `json:"Status"`
MacAddress string `json:"MacAddress"`
SourceDestCheck bool `json:"SourceDestCheck"`
VpcID string `json:"VpcId"`
Description string `json:"Description"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
NetworkInterfaceID string `json:"NetworkInterfaceId"`
PrivateIPAddresses []struct {
PrivateDNSName string `json:"PrivateDnsName"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
Primary bool `json:"Primary"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"PrivateIpAddresses"`
RequesterManaged bool `json:"RequesterManaged"`
Ipv6Addresses []interface{} `json:"Ipv6Addresses"`
PrivateDNSName string `json:"PrivateDnsName,omitempty"`
AvailabilityZone string `json:"AvailabilityZone"`
Attachment struct {
Status string `json:"Status"`
DeviceIndex int `json:"DeviceIndex"`
AttachTime time.Time `json:"AttachTime"`
InstanceID string `json:"InstanceId"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
AttachmentID string `json:"AttachmentId"`
InstanceOwnerID string `json:"InstanceOwnerId"`
} `json:"Attachment"`
Groups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"Groups"`
SubnetID string `json:"SubnetId"`
OwnerID string `json:"OwnerId"`
TagSet []interface{} `json:"TagSet"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"NetworkInterfaces"`
}
type DescribeInstances struct {
Reservations []struct {
Instances []struct {
AmiLaunchIndex int `json:"AmiLaunchIndex"`
ImageID string `json:"ImageId"`
InstanceID string `json:"InstanceId"`
InstanceType string `json:"InstanceType"`
KernelID string `json:"KernelId"`
KeyName string `json:"KeyName"`
LaunchTime string `json:"LaunchTime"`
Monitoring struct {
State string `json:"State"`
} `json:"Monitoring"`
Placement struct {
AvailabilityZone string `json:"AvailabilityZone"`
GroupName string `json:"GroupName"`
Tenancy string `json:"Tenancy"`
} `json:"Placement"`
Platform string `json:"Platform"`
PrivateDNS string `json:"PrivateDnsName"`
PrivateIP string `json:"PrivateIpAddress"`
PublicDNS string `json:"PublicDnsName"`
PublicIP string `json:"PublicIpAddress"`
State string `json:"State"`
SubnetID string `json:"SubnetId"`
VpcID string `json:"VpcId"`
Architecture string `json:"Architecture"`
BlockDeviceMappings []struct {
DeviceName string `json:"DeviceName"`
Ebs struct {
AttachTime time.Time `json:"AttachTime"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
Status string `json:"Status"`
VolumeID string `json:"VolumeId"`
} `json:"Ebs"`
} `json:"BlockDeviceMappings"`
ClientToken string `json:"ClientToken"`
EbsOptimized bool `json:"EbsOptimized"`
Hypervisor string `json:"Hypervisor"`
IamInstanceProfile struct {
Arn string `json:"Arn"`
ID string `json:"Id"`
} `json:"IamInstanceProfile"`
NetworkInterfaces []struct {
Attachment struct {
AttachTime time.Time `json:"AttachTime"`
AttachmentID string `json:"AttachmentId"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
DeviceIndex int `json:"DeviceIndex"`
Status string `json:"Status"`
} `json:"Attachment"`
Description string `json:"Description"`
Groups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"Groups"`
MacAddress string `json:"MacAddress"`
NetworkInterfaceID string `json:"NetworkInterfaceId"`
OwnerID string `json:"OwnerId"`
PrivateDNSName string `json:"PrivateDnsName"`
PrivateIPAddress string `json:"PrivateIpAddress"`
PrivateIPAddresses []struct {
Association struct {
IPOwnerID string `json:"IpOwnerId"`
PublicDNSName string `json:"PublicDnsName"`
PublicIP string `json:"PublicIp"`
} `json:"Association"`
Primary bool `json:"Primary"`
PrivateDNSName string `json:"PrivateDnsName"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"PrivateIpAddresses"`
SourceDestCheck bool `json:"SourceDestCheck"`
Status string `json:"Status"`
SubnetID string `json:"SubnetId"`
VpcID string `json:"VpcId"`
} `json:"NetworkInterfaces"`
RootDeviceName string `json:"RootDeviceName"`
RootDeviceType string `json:"RootDeviceType"`
SecurityGroups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"SecurityGroups"`
SourceDestCheck bool `json:"SourceDestCheck"`
StateReason struct {
Code string `json:"Code"`
Message string `json:"Message"`
} `json:"StateReason"`
Tags []struct {
Key string `json:"Key"`
Value string `json:"Value"`
} `json:"Tags"`
VirtualizationType string `json:"VirtualizationType"`
} `json:"Instances"`
OwnerID string `json:"OwnerId"`
RequesterID string `json:"RequesterId"`
ReservationID string `json:"ReservationId"`
} `json:"Reservations"`
}
type DescribeImages struct {
Images []struct {
Architecture string `json:"Architecture"`
BlockDeviceMappings []struct {
DeviceName string `json:"DeviceName"`
Ebs struct {
DeleteOnTermination bool `json:"DeleteOnTermination"`
SnapshotID string `json:"SnapshotId"`
VolumeSize int `json:"VolumeSize"`
VolumeType string `json:"VolumeType"`
} `json:"Ebs"`
} `json:"BlockDeviceMappings"`
CreationDate string `json:"CreationDate"`
Description string `json:"Description"`
EnaSupport bool `json:"EnaSupport"`
Hypervisor string `json:"Hypervisor"`
ImageID string `json:"ImageId"`
ImageLocation string `json:"ImageLocation"`
ImageType string `json:"ImageType"`
KernelID string `json:"KernelId"`
Name string `json:"Name"`
OwnerAlias string `json:"OwnerAlias"`
OwnerID string `json:"OwnerId"`
Platform string `json:"Platform"`
ProductCodes []struct {
ProductCodeID string `json:"ProductCodeId"`
ProductCodeType string `json:"ProductCodeType"`
} `json:"ProductCodes"`
Public bool `json:"Public"`
RamdiskID string `json:"RamdiskId"`
RootDeviceName string `json:"RootDeviceName"`
RootDeviceType string `json:"RootDeviceType"`
SriovNetSupport string `json:"SriovNetSupport"`
State string `json:"State"`
StateReason struct {
Code string `json:"Code"`
Message string `json:"Message"`
} `json:"StateReason"`
Tags []struct {
Key string `json:"Key"`
Value string `json:"Value"`
} `json:"Tags"`
VirtualizationType string `json:"VirtualizationType"`
} `json:"Images"`
NextToken string `json:"NextToken"`
}
type DescribeSnapshots struct {
Snapshots []struct {
DataEncryptionKeyID string `json:"DataEncryptionKeyId"`
Description string `json:"Description"`
Encrypted bool `json:"Encrypted"`
KMSKeyID string `json:"KmsKeyId"`
OwnerAlias string `json:"OwnerAlias"`
OwnerID string `json:"OwnerId"`
Progress string `json:"Progress"`
SnapshotID string `json:"SnapshotId"`
StartTime time.Time `json:"StartTime"`
State string `json:"State"`
StateMessage string `json:"StateMessage"`
TagSet []struct {
Key string `json:"Key"`
Value string `json:"Value"`
} `json:"Tags"`
VolumeID string `json:"VolumeId"`
VolumeSize int `json:"VolumeSize"`
} `json:"Snapshots"`
}
type DescribeVolumes struct {
Volumes []struct {
Attachments []struct {
AttachTime time.Time `json:"AttachTime"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
Device string `json:"Device"`
InstanceID string `json:"InstanceId"`
State string `json:"State"`
VolumeID string `json:"VolumeId"`
} `json:"Attachments"`
AvailabilityZone string `json:"AvailabilityZone"`
CreateTime string `json:"CreateTime"`
Encrypted bool `json:"Encrypted"`
KMSKeyID string `json:"KmsKeyId"`
Size int `json:"Size"`
SnapshotID string `json:"SnapshotId"`
State string `json:"State"`
VolumeID string `json:"VolumeId"`
} `json:"Volumes"`
}
// DescribeInstanceAttribute is a struct for the DescribeInstanceAttribute function - include userdata
type DescribeInstanceAttribute struct {
UserData string `json:"UserData"`
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)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
}
for _, mockednic := range c.describeNetworkInterfaces.NetworkInterfaces {
nics = append(nics, ec2types.NetworkInterface{
Association: &ec2types.NetworkInterfaceAssociation{
PublicIp: aws.String(mockednic.Association.PublicIP),
},
NetworkInterfaceId: aws.String(mockednic.NetworkInterfaceID),
PrivateIpAddress: aws.String(mockednic.PrivateIPAddress),
VpcId: aws.String(mockednic.VpcID),
Attachment: &ec2types.NetworkInterfaceAttachment{InstanceId: aws.String(mockednic.Attachment.InstanceID)},
Description: aws.String(mockednic.Description),
})
}
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
}
func (c *MockedEC2Client2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, f ...func(o *ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
var instances []ec2types.Instance
err := json.Unmarshal(readTestFile(DESCRIBE_INSTANCES_TEST_FILE), &c.describeInstances)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_INSTANCES_TEST_FILE)
}
for _, mockedReservation := range c.describeInstances.Reservations {
for _, mockedInstance := range mockedReservation.Instances {
for _, inputInstanceID := range input.InstanceIds {
if mockedInstance.InstanceID == inputInstanceID {
instances = append(instances, ec2types.Instance{
InstanceId: aws.String(mockedInstance.InstanceID),
InstanceType: ec2types.InstanceType(mockedInstance.InstanceType),
ImageId: aws.String(mockedInstance.ImageID),
})
}
}
}
}
return &ec2.DescribeInstancesOutput{Reservations: []ec2types.Reservation{
{Instances: instances},
}}, nil
}
func (c *MockedEC2Client2) DescribeVolumes(ctx context.Context, input *ec2.DescribeVolumesInput, f ...func(o *ec2.Options)) (*ec2.DescribeVolumesOutput, error) {
var volumes []ec2types.Volume
err := json.Unmarshal(readTestFile(DESCRIBE_VOLUMES_TEST_FILE), &c.describeVolumes)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_VOLUMES_TEST_FILE)
}
for _, mockedVolume := range c.describeVolumes.Volumes {
for _, inputVolumeID := range input.VolumeIds {
if mockedVolume.VolumeID == inputVolumeID {
volumes = append(volumes, ec2types.Volume{
VolumeId: aws.String(mockedVolume.VolumeID),
Size: aws.Int32(int32(mockedVolume.Size)),
Attachments: []ec2types.VolumeAttachment{
{
InstanceId: aws.String(mockedVolume.Attachments[0].InstanceID),
},
},
})
}
}
}
return &ec2.DescribeVolumesOutput{Volumes: volumes}, nil
}
func (c *MockedEC2Client2) DescribeSnapshots(ctx context.Context, input *ec2.DescribeSnapshotsInput, f ...func(o *ec2.Options)) (*ec2.DescribeSnapshotsOutput, error) {
var snapshots []ec2types.Snapshot
err := json.Unmarshal(readTestFile(DESCRIBE_SNAPSHOTS_TEST_FILE), &c.describeSnapshots)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_SNAPSHOTS_TEST_FILE)
}
for _, mockedSnapshot := range c.describeSnapshots.Snapshots {
for _, inputSnapshotID := range input.SnapshotIds {
if mockedSnapshot.SnapshotID == inputSnapshotID {
snapshots = append(snapshots, ec2types.Snapshot{
SnapshotId: aws.String(mockedSnapshot.SnapshotID),
VolumeId: aws.String(mockedSnapshot.VolumeID),
State: ec2types.SnapshotState(mockedSnapshot.State),
})
}
}
}
return &ec2.DescribeSnapshotsOutput{Snapshots: snapshots}, nil
}
func (c *MockedEC2Client2) DescribeImages(ctx context.Context, input *ec2.DescribeImagesInput, f ...func(o *ec2.Options)) (*ec2.DescribeImagesOutput, error) {
var images []ec2types.Image
err := json.Unmarshal(readTestFile(DESCRIBE_IMAGES_TEST_FILE), &c.describeImages)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_IMAGES_TEST_FILE)
}
for _, mockedImage := range c.describeImages.Images {
for _, inputImageID := range input.ImageIds {
if mockedImage.ImageID == inputImageID {
images = append(images, ec2types.Image{
ImageId: aws.String(mockedImage.ImageID),
Name: aws.String(mockedImage.Name),
})
}
}
}
return &ec2.DescribeImagesOutput{Images: images}, nil
}
func (c *MockedEC2Client2) DescribeInstanceAttribute(ctx context.Context, input *ec2.DescribeInstanceAttributeInput, f ...func(o *ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error) {
return &ec2.DescribeInstanceAttributeOutput{
UserData: &ec2types.AttributeValue{
Value: aws.String("userdata"),
},
}, 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
}
+5 -44
View File
@@ -11,7 +11,6 @@ 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 {
@@ -20,10 +19,9 @@ type AWSECRClientInterface interface {
GetRepositoryPolicy(ctx context.Context, params *ecr.GetRepositoryPolicyInput, optFns ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error)
}
func init() {
func RegisterECRTypes() {
gob.Register([]ecrTypes.Repository{})
gob.Register([]ecrTypes.ImageDetail{})
gob.Register(ecrTypes.Repository{})
}
@@ -34,20 +32,9 @@ func CachedECRDescribeRepositories(ECRClient AWSECRClientInterface, accountID st
cacheKey := fmt.Sprintf("%s-ecr-DescribeRepositories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeRepositories",
"account": accountID,
"region": region,
"cache": "hit",
}).Info("AWS API call")
sharedLogger.Debug("Using cached ECR repositories data")
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(
@@ -82,25 +69,12 @@ func CachedECRDescribeRepositories(ECRClient AWSECRClientInterface, accountID st
func CachedECRDescribeImages(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) ([]ecrTypes.ImageDetail, error) {
var PaginationControl *string
var images []ecrTypes.ImageDetail
cacheKey := fmt.Sprintf("%s-ecr-DescribeImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
cacheKey := fmt.Sprintf("%s-efs-DescribImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.WithFields(logrus.Fields{
"api": "ecr:DescribeImages",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "hit",
}).Info("AWS API call")
sharedLogger.Debug("Using cached Images data")
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(
@@ -138,22 +112,9 @@ 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.WithFields(logrus.Fields{
"api": "ecr:GetRepositoryPolicy",
"account": accountID,
"region": region,
"repository": repositoryName,
"cache": "hit",
}).Info("AWS API call")
sharedLogger.Debug("Using cached ECR repository policy data")
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(),
-109
View File
@@ -1,109 +0,0 @@
package sdk
import (
"context"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
)
type MockedECRClient struct {
}
func (m *MockedECRClient) DescribeRepositories(ctx context.Context, input *ecr.DescribeRepositoriesInput, options ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) {
return &ecr.DescribeRepositoriesOutput{
Repositories: []ecrTypes.Repository{
{
RepositoryName: aws.String("repo1"),
RepositoryUri: aws.String("11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo1"),
},
{
RepositoryName: aws.String("repo2"),
RepositoryUri: aws.String("11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo2"),
},
},
}, nil
}
func (m *MockedECRClient) DescribeImages(ctx context.Context, input *ecr.DescribeImagesInput, options ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) {
if aws.ToString(input.RepositoryName) == "repo1" {
return &ecr.DescribeImagesOutput{
ImageDetails: []ecrTypes.ImageDetail{
{
ImageTags: []string{
"customtag",
"tag2",
},
ImagePushedAt: aws.Time(time.Date(2022, 10, 25, 15, 14, 0, 0, time.UTC)),
ImageSizeInBytes: aws.Int64(123456),
},
},
}, nil
} else if aws.ToString(input.RepositoryName) == "repo2" {
return &ecr.DescribeImagesOutput{
ImageDetails: []ecrTypes.ImageDetail{
{
ImageTags: []string{
"latest",
},
ImagePushedAt: aws.Time(time.Date(2021, 10, 15, 11, 14, 0, 0, time.UTC)),
ImageSizeInBytes: aws.Int64(2222222),
},
},
}, nil
} else {
return &ecr.DescribeImagesOutput{
ImageDetails: []ecrTypes.ImageDetail{
{
ImageTags: []string{
"customtag",
"tag2",
},
ImagePushedAt: aws.Time(time.Date(2022, 10, 25, 15, 14, 0, 0, time.UTC)),
ImageSizeInBytes: aws.Int64(111),
},
{
ImageTags: []string{
"latest",
},
ImagePushedAt: aws.Time(time.Date(2021, 10, 15, 11, 14, 0, 0, time.UTC)),
ImageSizeInBytes: aws.Int64(333),
},
},
}, nil
}
}
func (m *MockedECRClient) GetRepositoryPolicy(ctx context.Context, input *ecr.GetRepositoryPolicyInput, options ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) {
return &ecr.GetRepositoryPolicyOutput{
PolicyText: aws.String(`{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "AllowPushPull",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::123456789012:root",
"arn:aws:iam::123456789012:user/MyUser"
]
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
}
]
}`),
}, nil
}
-286
View File
@@ -1,286 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"strings"
"github.com/BishopFox/cloudfox/internal"
"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 {
ListClusters(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error)
ListTasks(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error)
ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error)
DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error)
DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
}
func init() {
gob.Register([]string{})
gob.Register(ecsTypes.Task{})
gob.Register([]ecsTypes.Task{})
gob.Register(ecsTypes.TaskDefinition{})
}
func CachedECSListClusters(ECSClient AWSECSClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var clusters []string
cacheKey := fmt.Sprintf("%s-ecs-ListClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
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(
context.TODO(),
&ecs.ListClustersInput{
NextToken: PaginationControl,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
clusters = append(clusters, ListClusters.ClusterArns...)
// Pagination control.
if ListClusters.NextToken != nil {
PaginationControl = ListClusters.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
return clusters, nil
}
func CachedECSListTasks(ECSClient AWSECSClientInterface, accountID string, region string, cluster string) ([]string, error) {
var PaginationControl *string
var tasks []string
//grab cluster name from AWS ARN
clusterName := cluster[strings.LastIndex(cluster, "/")+1:]
cacheKey := fmt.Sprintf("%s-ecs-ListTasks-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
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(
context.TODO(),
&ecs.ListTasksInput{
Cluster: &cluster,
NextToken: PaginationControl,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
tasks = append(tasks, ListTasks.TaskArns...)
// Pagination control.
if ListTasks.NextToken != nil {
PaginationControl = ListTasks.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, tasks, cache.DefaultExpiration)
return tasks, nil
}
func CachedECSDescribeTasks(ECSClient AWSECSClientInterface, accountID string, region string, cluster string, tasks []string) ([]ecsTypes.Task, error) {
var taskDetails []ecsTypes.Task
//replace semi-colons with underscores in task definition name
clusterFileSystemSafe := strings.ReplaceAll(cluster, ":", "_")
clusterFileSystemSafe = strings.ReplaceAll(clusterFileSystemSafe, "/", "_")
cacheKey := fmt.Sprintf("%s-ecs-DescribeTasks-%s-%s", accountID, region, clusterFileSystemSafe)
cached, found := internal.Cache.Get(cacheKey)
if found {
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(),
&ecs.DescribeTasksInput{
Cluster: &cluster,
Tasks: tasks,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return []ecsTypes.Task{}, err
}
taskDetails = append(taskDetails, DescribeTasks.Tasks...)
internal.Cache.Set(cacheKey, taskDetails, cache.DefaultExpiration)
return taskDetails, nil
}
func CachedECSDescribeTaskDefinition(ECSClient AWSECSClientInterface, accountID string, region string, taskDefinition string) (ecsTypes.TaskDefinition, error) {
var taskDefinitionDetails ecsTypes.TaskDefinition
//replace semi-colons with underscores in task definition name
taskDefinitionFileSystemSafe := strings.ReplaceAll(taskDefinition, ":", "_")
taskDefinitionFileSystemSafe = strings.ReplaceAll(taskDefinitionFileSystemSafe, "/", "_")
cacheKey := fmt.Sprintf("%s-ecs-DescribeTaskDefinition-%s-%s", accountID, region, taskDefinitionFileSystemSafe)
cached, found := internal.Cache.Get(cacheKey)
if found {
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(),
&ecs.DescribeTaskDefinitionInput{
TaskDefinition: &taskDefinition,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return ecsTypes.TaskDefinition{}, err
}
taskDefinitionDetails = *DescribeTaskDefinition.TaskDefinition
internal.Cache.Set(cacheKey, taskDefinitionDetails, cache.DefaultExpiration)
return taskDefinitionDetails, nil
}
func CachedECSListServices(ECSClient AWSECSClientInterface, accountID string, region string, cluster string) ([]string, error) {
var PaginationControl *string
var services []string
//grab cluster name from AWS ARN
clusterName := cluster[strings.LastIndex(cluster, "/")+1:]
cacheKey := fmt.Sprintf("%s-ecs-ListServices-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
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(
context.TODO(),
&ecs.ListServicesInput{
Cluster: &cluster,
NextToken: PaginationControl,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
services = append(services, ListServices.ServiceArns...)
// Pagination control.
if ListServices.NextToken != nil {
PaginationControl = ListServices.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
return services, nil
}
-174
View File
@@ -1,174 +0,0 @@
package sdk
import (
"context"
"encoding/json"
"log"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecs"
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
)
const DESCRIBE_TASKS_TEST_FILE = "./test-data/describe-tasks.json"
type MockedECSClient struct {
describeTasks DescribeTasks
}
type ListTasks struct {
TaskArns []string `json:"taskArns"`
}
type DescribeTasks struct {
Tasks []struct {
Attachments []struct {
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
Details []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"details"`
} `json:"attachments"`
Attributes []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"attributes"`
AvailabilityZone string `json:"availabilityZone"`
ClusterArn string `json:"clusterArn"`
Connectivity string `json:"connectivity"`
ConnectivityAt string `json:"connectivityAt"`
Containers []struct {
ContainerArn string `json:"containerArn"`
TaskArn string `json:"taskArn"`
Name string `json:"name"`
Image string `json:"image"`
RuntimeID string `json:"runtimeId"`
LastStatus string `json:"lastStatus"`
NetworkBindings []interface{} `json:"networkBindings"`
NetworkInterfaces []struct {
AttachmentID string `json:"attachmentId"`
PrivateIpv4Address string `json:"privateIpv4Address"`
} `json:"networkInterfaces"`
HealthStatus string `json:"healthStatus"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
} `json:"containers"`
CPU string `json:"cpu"`
CreatedAt string `json:"createdAt"`
DesiredStatus string `json:"desiredStatus"`
EnableExecuteCommand bool `json:"enableExecuteCommand"`
Group string `json:"group"`
HealthStatus string `json:"healthStatus"`
LastStatus string `json:"lastStatus"`
LaunchType string `json:"launchType"`
Memory string `json:"memory"`
Overrides struct {
ContainerOverrides []struct {
Name string `json:"name"`
} `json:"containerOverrides"`
InferenceAcceleratorOverrides []interface{} `json:"inferenceAcceleratorOverrides"`
} `json:"overrides"`
PlatformVersion string `json:"platformVersion"`
PlatformFamily string `json:"platformFamily"`
PullStartedAt string `json:"pullStartedAt"`
PullStoppedAt string `json:"pullStoppedAt"`
StartedAt string `json:"startedAt"`
StartedBy string `json:"startedBy"`
Tags []interface{} `json:"tags"`
TaskArn string `json:"taskArn"`
TaskDefinitionArn string `json:"taskDefinitionArn"`
Version int `json:"version"`
EphemeralStorage struct {
SizeInGiB int `json:"sizeInGiB"`
} `json:"ephemeralStorage"`
} `json:"tasks"`
Failures []interface{} `json:"failures"`
}
func (c *MockedECSClient) ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
return &ecs.ListClustersOutput{ClusterArns: []string{
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster2",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster3",
}}, nil
}
func (c *MockedECSClient) ListTasks(ctx context.Context, input *ecs.ListTasksInput, f ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
return &ecs.ListTasksOutput{TaskArns: []string{
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE",
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE",
}}, nil
}
func (c *MockedECSClient) ListServices(ctx context.Context, input *ecs.ListServicesInput, f ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) {
return &ecs.ListServicesOutput{ServiceArns: []string{
"arn:aws:ecs:us-east-1:123456789012:service/MyService",
"arn:aws:ecs:us-east-1:123456789012:service/MyService2",
"arn:aws:ecs:us-east-1:123456789012:service/MyService3",
}}, nil
}
func (c *MockedECSClient) DescribeTasks(ctx context.Context, input *ecs.DescribeTasksInput, f ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_TASKS_TEST_FILE), &c.describeTasks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_TASKS_TEST_FILE)
}
var tasks []ecsTypes.Task
for _, mockedTask := range c.describeTasks.Tasks {
if mockedTask.ClusterArn == aws.ToString(input.Cluster) {
for _, inputTask := range input.Tasks {
if mockedTask.TaskArn == inputTask {
var attachments []ecsTypes.Attachment
for _, a := range mockedTask.Attachments {
var deets []ecsTypes.KeyValuePair
for _, detail := range a.Details {
deets = append(deets, ecsTypes.KeyValuePair{
Name: aws.String(detail.Name),
Value: aws.String(detail.Value)})
}
attachments = append(attachments, ecsTypes.Attachment{
Type: aws.String(a.Type),
Details: deets,
Id: aws.String(a.ID),
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,
})
}
}
}
}
return &ecs.DescribeTasksOutput{Tasks: tasks}, nil
}
func (c *MockedECSClient) DescribeTaskDefinition(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, f ...func(o *ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) {
testTaskDefinition := ecsTypes.TaskDefinition{}
testTaskDefinition.TaskRoleArn = aws.String("test123")
return &ecs.DescribeTaskDefinitionOutput{TaskDefinition: &testTaskDefinition}, nil
}
+1 -50
View File
@@ -11,7 +11,6 @@ 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 {
@@ -21,7 +20,7 @@ type AWSEFSClientInterface interface {
DescribeFileSystemPolicy(ctx context.Context, params *efs.DescribeFileSystemPolicyInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemPolicyOutput, error)
}
func init() {
func RegisterEFSTypes() {
gob.Register([]efsTypes.FileSystemDescription{})
gob.Register([]efsTypes.MountTargetDescription{})
gob.Register([]efsTypes.AccessPointDescription{})
@@ -35,20 +34,8 @@ 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(
@@ -86,20 +73,8 @@ 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(
@@ -138,20 +113,8 @@ 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(),
@@ -188,20 +151,8 @@ 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(),
-67
View File
@@ -1,67 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/efs"
efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types"
)
type MockedEfsClient struct {
}
func (m *MockedEfsClient) DescribeFileSystems(ctx context.Context, input *efs.DescribeFileSystemsInput, options ...func(*efs.Options)) (*efs.DescribeFileSystemsOutput, error) {
return &efs.DescribeFileSystemsOutput{
FileSystems: []efsTypes.FileSystemDescription{
{
FileSystemId: aws.String("fs-12345678"),
},
{
FileSystemId: aws.String("fs-87654321"),
},
},
}, nil
}
func (m *MockedEfsClient) DescribeMountTargets(ctx context.Context, input *efs.DescribeMountTargetsInput, options ...func(*efs.Options)) (*efs.DescribeMountTargetsOutput, error) {
return &efs.DescribeMountTargetsOutput{
MountTargets: []efsTypes.MountTargetDescription{
{
MountTargetId: aws.String("fsmt-12345678"),
FileSystemId: aws.String("fs-12345678"),
IpAddress: aws.String("10.1.1.1"),
},
{
MountTargetId: aws.String("fsmt-87654321"),
FileSystemId: aws.String("fs-87654321"),
IpAddress: aws.String("10.2.2.2.2"),
},
},
}, nil
}
func (m *MockedEfsClient) DescribeAccessPoints(ctx context.Context, input *efs.DescribeAccessPointsInput, options ...func(*efs.Options)) (*efs.DescribeAccessPointsOutput, error) {
return &efs.DescribeAccessPointsOutput{
AccessPoints: []efsTypes.AccessPointDescription{
{
AccessPointId: aws.String("fsap-12345678"),
FileSystemId: aws.String("fs-12345678"),
Name: aws.String("fsap-12345678"),
PosixUser: &efsTypes.PosixUser{
Gid: aws.Int64(1000),
Uid: aws.Int64(1000),
},
},
{
AccessPointId: aws.String("fsap-87654321"),
FileSystemId: aws.String("fs-87654321"),
Name: aws.String("fsap-12345679"),
PosixUser: &efsTypes.PosixUser{
Gid: aws.Int64(1000),
Uid: aws.Int64(1000),
},
},
},
}, nil
}
+2 -59
View File
@@ -9,7 +9,6 @@ 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 {
@@ -19,7 +18,7 @@ type EKSClientInterface interface {
ListNodegroups(context.Context, *eks.ListNodegroupsInput, ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error)
}
func init() {
func RegisterEKSTypes() {
gob.Register([]string{})
gob.Register(eksTypes.Cluster{})
gob.Register(eksTypes.Nodegroup{})
@@ -31,22 +30,8 @@ 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{
@@ -72,20 +57,8 @@ 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(),
@@ -120,22 +93,8 @@ 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(),
@@ -167,27 +126,11 @@ func CachedEKSListNodeGroups(client EKSClientInterface, accountID string, region
func CachedEKSDescribeNodeGroup(client EKSClientInterface, accountID string, region string, clusterName string, nodeGroupName string) (eksTypes.Nodegroup, error) {
var nodeGroup eksTypes.Nodegroup
cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, clusterName, nodeGroupName)
cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, 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{
-54
View File
@@ -1,54 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/eks"
eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
)
type MockedAWSEksClient struct {
}
func (m *MockedAWSEksClient) ListClusters(ctx context.Context, input *eks.ListClustersInput, options ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
return &eks.ListClustersOutput{
Clusters: []string{
"cluster1",
"cluster2",
},
}, nil
}
func (m *MockedAWSEksClient) DescribeCluster(ctx context.Context, input *eks.DescribeClusterInput, options ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
return &eks.DescribeClusterOutput{
Cluster: &eksTypes.Cluster{
Arn: aws.String("arn:aws:eks:us-east-1:123456789012:cluster/cluster1"),
Endpoint: aws.String("https://cluster1.us-east-1.eks.amazonaws.com"),
Name: aws.String("cluster1"),
PlatformVersion: aws.String("eks.1"),
Version: aws.String("1.18"),
RoleArn: aws.String("arn:aws:iam::123456789012:role/eks-role"),
},
}, nil
}
func (m *MockedAWSEksClient) ListNodegroups(ctx context.Context, input *eks.ListNodegroupsInput, options ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
return &eks.ListNodegroupsOutput{
Nodegroups: []string{
"nodegroup1",
"nodegroup2",
},
}, nil
}
func (m *MockedAWSEksClient) DescribeNodegroup(ctx context.Context, input *eks.DescribeNodegroupInput, options ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
return &eks.DescribeNodegroupOutput{
Nodegroup: &eksTypes.Nodegroup{
ClusterName: aws.String("cluster1"),
NodeRole: aws.String("arn:aws:iam::123456789012:role/eks-role"),
NodegroupName: aws.String("nodegroup1"),
NodegroupArn: aws.String("arn:aws:eks:us-east-1:123456789012:nodegroup/cluster1/nodegroup1"),
},
}, nil
}
-70
View File
@@ -1,70 +0,0 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"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 {
DescribeCacheClusters(context.Context, *elasticache.DescribeCacheClustersInput, ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error)
}
func init() {
gob.Register([]elasticacheTypes.CacheCluster{})
}
func CachedElastiCacheDescribeCacheClusters(client AWSElastiCacheClientInterface, accountID string, region string) ([]elasticacheTypes.CacheCluster, error) {
var PaginationControl *string
var clusters []elasticacheTypes.CacheCluster
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(),
&elasticache.DescribeCacheClustersInput{
Marker: PaginationControl,
},
func(o *elasticache.Options) {
o.Region = region
},
)
if err != nil {
return clusters, err
}
clusters = append(clusters, DescribeCacheClusters.CacheClusters...)
//pagination
if DescribeCacheClusters.Marker == nil {
break
}
PaginationControl = DescribeCacheClusters.Marker
}
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
return clusters, nil
}
-40
View File
@@ -1,40 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/elasticache"
elasticacheTypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
)
type MockedElasticacheClient struct {
}
func (m *MockedElasticacheClient) DescribeCacheClusters(ctx context.Context, input *elasticache.DescribeCacheClustersInput, options ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) {
return &elasticache.DescribeCacheClustersOutput{
CacheClusters: []elasticacheTypes.CacheCluster{
{
CacheClusterId: aws.String("test"),
ARN: aws.String("arn:aws:elasticache:us-east-1:123456789012:cluster:myCluster"),
Engine: aws.String("redis"),
EngineVersion: aws.String("6.x"),
CacheNodeType: aws.String("cache.t3.micro"),
NumCacheNodes: aws.Int32(1),
CacheClusterStatus: aws.String("available"),
PreferredAvailabilityZone: aws.String("us-east-1a"),
CacheSubnetGroupName: aws.String("default"),
ReplicationGroupId: aws.String("test"),
SecurityGroups: []elasticacheTypes.SecurityGroupMembership{
{
SecurityGroupId: aws.String("test"),
Status: aws.String("active"),
},
},
AutoMinorVersionUpgrade: aws.Bool(true),
PreferredMaintenanceWindow: aws.String("sun:05:00-sun:06:00"),
},
},
}, nil
}
-57
View File
@@ -1,57 +0,0 @@
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 {
DescribeApplications(context.Context, *elasticbeanstalk.DescribeApplicationsInput, ...func(*elasticbeanstalk.Options)) (*elasticbeanstalk.DescribeApplicationsOutput, error)
}
func init() {
gob.Register([]elasticbeanstalkTypes.ApplicationDescription{})
}
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{},
func(o *elasticbeanstalk.Options) {
o.Region = region
},
)
if err != nil {
return applications, err
}
applications = append(applications, DescribeApplications.Applications...)
internal.Cache.Set(cacheKey, applications, cache.DefaultExpiration)
return applications, nil
}
-33
View File
@@ -1,33 +0,0 @@
package sdk
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk"
elasticbeanstalkTypes "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk/types"
)
type MockedElasticBeanstalkClient struct {
}
func (m *MockedElasticBeanstalkClient) DescribeApplications(ctx context.Context, input *elasticbeanstalk.DescribeApplicationsInput, options ...func(*elasticbeanstalk.Options)) (*elasticbeanstalk.DescribeApplicationsOutput, error) {
return &elasticbeanstalk.DescribeApplicationsOutput{
Applications: []elasticbeanstalkTypes.ApplicationDescription{
{
ApplicationName: aws.String("app1"),
ApplicationArn: aws.String("arn:aws:elasticbeanstalk:us-east-1:123456789012:application/app1"),
ConfigurationTemplates: []string{
"template1",
},
},
{
ApplicationName: aws.String("app2"),
ApplicationArn: aws.String("arn:aws:elasticbeanstalk:us-east-1:123456789012:application/app2"),
ConfigurationTemplates: []string{
"template2",
},
},
},
}, nil
}
Loaded 100 of 431 files, more files were not shown because too many files have changed in this diff. Show more