Compare commits

..
120 Commits
Author SHA1 Message Date
sethsec-bf a8d2bfbfb3 Removed graph command from cobra for now 2024-04-16 14:18:47 -04:00
sethsec-bf c0e4301ef3 Add GCP to readme, fix typo 2024-04-16 14:10:51 -04:00
sethsec-bf 22417f174b spelling 2024-04-16 11:54:10 -04:00
sethsec-bf c3be95b3db cleaned up enhanced pmapper loot file 2024-04-16 11:47:31 -04:00
sethsec-bf abcc930f20 added afero fs back to output2 (needed to pass brew tests) 2024-04-16 11:29:53 -04:00
sethsec-bf 77cd08b8d5 updated gcp verbosity, updated cape command usage, switched version tracking file from main.go to internal/utils.go 2024-04-16 11:24:18 -04:00
sethsec-bf 57194ef3e0 bump to version 1.14.0, merged gcp and aws ds functionality 2024-04-16 10:32:19 -04:00
sethsec-bf 82635fd44f made aws sso like eks, where edges are not created if it's if the provider is in the same account as the role that trusts it. the edges will still show up cross account though. 2024-04-16 10:28:15 -04:00
sethsec-bf 67af1bd7fb update gcp package with vuln 2024-04-16 10:23:06 -04:00
sethsec-bf 79f3899ead update go mod 2024-04-16 10:21:33 -04:00
Seth Art 8ab4c4200a Merge branch 'main' into bastien_directoryservice_aws 2024-04-15 20:30:27 -04:00
sethsec-bf 6e52e5ee7b merge gcp stuff from main into this branch 2024-04-15 16:33:09 -04:00
Joe 9920c411c4 Merge pull request #82 from BishopFox/codeowners
Update codeowners
2024-04-15 10:58:45 -07:00
moloch-- df3fb6eae0 Update codeowners 2024-04-15 10:58:21 -07:00
David 8e94b3c46a Merge pull request #79 from BishopFox/feature/gcp-v1
Cloudfox GCP Functionality
2024-04-12 11:43:43 -04:00
David 94628aae73 i cant spel 2024-04-12 01:12:19 +00:00
David 23e8878e49 Merge branch 'main' into feature/gcp-v1 2024-04-12 01:04:49 +00:00
sethsec-bf 356d57b2de Added pmapper basepath to all relevent commands. Improved logging for cape/cape-tui. Fixed codebuld cache bug. 2024-04-10 11:49:33 -04:00
sethsec-bf 3a45583015 Got cape working without any aws calls, cleaned up logging messages 2024-04-05 16:07:37 -04:00
Bastien Faure 57832ada7c Codespell fix 2024-04-02 12:01:31 -07:00
Bastien Faure 608f07804e AWS uses a mix of clouddirectory and directoryservices for Directory services 2024-04-02 12:00:45 -07:00
Bastien Faure de492fa6e9 Add Directory Service support for AWS 2024-04-02 11:49:45 -07:00
David dee22b3ebc fix logging issue and improve whoami 2024-03-29 19:44:17 +00:00
sethsec-bf 590a94b576 changed println to printf 2024-03-28 15:10:22 -04:00
sethsec-bf fca22ceb6b renamed to cape, added hop count logic, pulled privesc function out so i can add logic to handle cobra flags 2024-03-28 15:06:41 -04:00
sethsec-bf 84369247a7 quick fix for context error. logging issue still there. 2024-03-22 14:43:52 -04:00
sethsec-bf 4ef87d8b98 updated caper to use new version of parseFederatedRoleTrusts from the role-trusts command. Also changed the way vendors and federated identities are labled 2024-03-22 08:58:05 -04:00
David 334583d3a2 save work whomai account stub 2024-03-21 22:38:02 +00:00
sethsec-bf 8fac75ed11 merged from main 2024-03-21 09:58:35 -04:00
sethsec-bf 3ff704e98f merged from main 2024-03-21 09:56:18 -04:00
Seth Art b73cd90323 Merge pull request #78 from enzowritescode/feature/aws-neptune
Feature/aws neptune
2024-03-20 15:23:20 -04:00
sethsec-bf ce0310675d update release and fix database test 2024-03-20 15:22:04 -04:00
sethsec-bf f9d9ec4bb8 Merge branch 'main' of github.com:BishopFox/cloudfox into feature/aws-neptune 2024-03-20 15:18:22 -04:00
Seth Art 125c1ea0cb Merge pull request #80 from BishopFox/seth-dev
Fix API gatway bug and improve role trust processing
2024-03-15 16:09:34 -04:00
sethsec-bf 00e63b0e18 found a way to fix the apigateway types conflict with gob 2024-03-15 16:05:43 -04:00
sethsec-bf eaac503bff bumped version to 1.13.4 for release with apigateway fix 2024-03-15 11:35:29 -04:00
sethsec-bf cec0d49602 Fixing a bug in the new cached versions of the apigateway sdk calls 2024-03-15 11:28:35 -04:00
sethsec-bf 6a21b9665d Auth0 not ready yet - also this new version lists unknown federated types not instead of ignoring them 2024-03-14 14:49:04 -04:00
sethsec-bf a1903f67f7 Major update for role trusts parsing. Cleaner version has case statement on federated principal value and not soley on condition data 2024-03-14 14:47:17 -04:00
David 2f99a7642f working gcp functionality 2024-03-13 14:19:47 +00:00
sethsec-bf 8d039323bf Fix for #77 2024-03-08 09:01:59 -05:00
sethsec-bf 05fd899219 Fixed bug in federeated role trust poclies where multiple subjects are trusted 2024-03-07 15:56:13 -05:00
enzowritescode a40f0aa179 More cleanup 2024-02-28 15:05:38 -07:00
enzowritescode 23409e3b82 remove unused code 2024-02-28 14:59:50 -07:00
sethsec-bf 36c9a5b795 Merged changes from neptune PR into this branch 2024-02-28 09:34:41 -05:00
sethsec-bf fd01647a8a Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2024-02-28 09:32:25 -05:00
Seth Art f415f991c8 Merge pull request #74 from enzowritescode/feature/aws-neptune
Amazon Neptune Support
2024-02-27 12:24:16 -05:00
Seth Art 15e0507ad6 Merge branch 'main' into feature/aws-neptune 2024-02-27 10:13:10 -05:00
sethsec-bf 80c00f3d97 Added test for databases command 2024-02-27 10:10:56 -05:00
sethsec-bf 59afbb381e Switched RDS database command from instances to clusters, and since it grabs Neptune and DocsDB clusters, we don't need to run those api calls. (they all return the same data). Also added back port info and added role info to the RDS clusters in -o wide mode 2024-02-23 13:14:09 -05:00
enzowritescode ca437d3626 Filter Neptune results 2024-02-19 13:05:05 -07:00
sethsec-bf dd6dd291c5 playing around with saving graph state between runs 2024-02-13 16:20:37 -05:00
sethsec-bf 42b0f3a212 Merge branch 'main' of github.com:BishopFox/cloudfox into graph 2024-02-08 16:57:03 -05:00
sethsec-bf e4e548012f revert test 2024-02-08 16:56:51 -05:00
sethsec-bf 1ca5dbfb10 Fix bug in role-trusts command around new vendor lookup feature, enabled caching on apigateway commands 2024-02-08 16:49:50 -05:00
sethsec-bf 24b7076bc3 saving place in caper command 2024-02-08 16:32:15 -05:00
Seth Art f00044670b Merge pull request #75 from BishopFox/seth-dev
Version 1.13.2 - 2 bug fixes on new functionality in 1.13.1
2024-02-02 13:48:47 -05:00
sethsec-bf ae1dac6c21 Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2024-02-02 13:46:29 -05:00
sethsec-bf 237b0735a8 Bumped version to 1.13.2 2024-02-02 13:45:41 -05:00
sethsec-bf 18e38bf1ea Fixed bug in env-vars command introduced in 1.13.1 with the new interesting version of the table written to disk. was still recording them all. now the second table only has interesting env-vars 2024-02-02 13:42:37 -05:00
sethsec-bf b5908fc720 Fixed bug in the role trusts command introduced in 1.13.1 where cloudfox only shows princiapls with :root trust and not ALL role trusts 2024-02-02 13:29:33 -05:00
sethsec-bf e4f3421ea1 Added functionailty to hightlight admins in caper command 2024-02-02 13:23:49 -05:00
sethsec-bf e61a401733 Kept first draft as the graph command. Moved second take to the caper command. 2024-01-31 16:47:29 -05:00
enzowritescode 29514a96b5 Merge in latest and fix merge conflicts 2024-01-25 19:33:19 -07:00
Seth Art b2c450ee02 Merge pull request #73 from BishopFox/seth-dev
Release PR for 1.13.1
2024-01-25 20:31:07 -05:00
sethsec-bf a65610390b Bumped to version 1.31.1 before PR 2024-01-25 20:26:49 -05:00
sethsec-bf 997c13d4de Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2024-01-25 20:25:45 -05:00
sethsec-bf 1d1d198a01 More fixes from #72. Took the suggestion from @johnkeates and fixed the prepopulated nmap commands to NOT include a profile for the cases where the user did not specify a profile for cloudfox 2024-01-25 20:23:03 -05:00
sethsec-bf 6069267758 Fix for sub issue found by @cyberbutler in #72 2024-01-25 16:42:37 -05:00
sethsec-bf a13527a7a9 Added MakeVertices method for type Role 2024-01-25 16:25:11 -05:00
sethsec-bf e08967bb31 This potential fix for #72 makes a lot more sense. Rather than overrwite the profile attribute of the struct, i have an AWSProfileProvided and a AWSProfileFake so that I can just pass the orig to other modules (since each module cleans it up itself). 2024-01-25 15:47:38 -05:00
sethsec-bf aeccb6db6d Have global data in dom's graph format now. just need to write the table creation code 2024-01-25 15:24:49 -05:00
Seth Art 8e99347789 Update README.md 2024-01-23 21:50:00 -05:00
Seth Art ff810ba17d Update README.md 2024-01-23 09:50:26 -05:00
sethsec-bf 4d3c6a4c67 Merge remote-tracking branch 'origin/seth-dev' into graph 2024-01-22 21:26:47 -05:00
Seth Art 23fc346e4f Update README.md 2024-01-22 21:20:51 -05:00
sethsec-bf 89789a05c1 updated pmapper command info 2024-01-22 21:16:25 -05:00
sethsec-bf 5827abb2a2 Hopefully this is a fix for #72 2024-01-22 20:47:11 -05:00
sethsec-bf 13a03c5de2 Fixed panic bug that occured when user specified a profile that did not exist 2024-01-22 20:43:36 -05:00
sethsec-bf ac1125d47e added users model 2024-01-22 15:46:19 -05:00
sethsec-bf dd6bfefa9d Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2024-01-22 15:44:59 -05:00
sethsec-bf 009321e7cb Updated pmapper output files 2024-01-22 15:44:43 -05:00
enzowritescode 8c4bcc035b Add .idea to gitignore for GoLand 2024-01-20 19:55:15 -07:00
enzowritescode a38451c1cd Typo fixes, reduced copy pasta, Neptune support 2024-01-20 19:35:13 -07:00
sethsec-bf 4cf58a3479 Created loot file for pmapper 2024-01-18 10:06:41 -05:00
sethsec-bf f5437b7afb Started to add knownvendoraccounts info 2024-01-17 09:03:48 -05:00
Seth Art 5df1ca6d75 Update README.md 2024-01-06 10:23:22 -05:00
sethsec-bf ef25d8b04d Merged origin/seth-dev into graph 2024-01-03 09:10:16 -05:00
sethsec-bf ea2ee3dd2c Fixed bug where instances without instance profile were not showing up 2024-01-02 21:22:55 -05:00
sethsec-bf 97e2fef388 Add data from fwd:cloudsec's known_aws_accounts repo into role-trust module 2024-01-02 21:22:27 -05:00
sethsec-bf 1d53b984ac Used the output2 loot mechanism for api-gws, updated tests 2023-12-28 12:11:07 -05:00
sethsec-bf 525f262460 Added mocks for apigw and apigwv2, and a test for the new api-gw command 2023-12-27 14:54:03 -05:00
sethsec-bf f13df0708c Added mocks for apigw and apigwv2, and a test for the new api-gw command 2023-12-27 14:53:38 -05:00
Seth Art 7520999386 Merge pull request #67 from BishopFox/seth-dev
v1.13.0
2023-12-26 13:04:20 -05:00
sethsec-bf f1b6eae9f1 codespell was tripping up on a has from go.sum 2023-12-26 13:00:34 -05:00
sethsec-bf 06f8e36be2 bumped version to 1.13.0 2023-12-26 12:03:03 -05:00
sethsec-bf cba252d151 Normalize/clean up console messages. Also re-order the way the messages print in multi-profile mode 2023-12-26 12:02:26 -05:00
sethsec-bf 5c4d0fda89 go get -u and fixes 2023-12-26 10:54:39 -05:00
sethsec-bf d648e5b347 Added ability to supply just a princpal name, and the command will try it as a role and a user 2023-12-26 10:34:55 -05:00
sethsec-bf d1af1bbf8f Fixed a bug where permissions custom runs were overwriting the files for the default run. 2023-12-26 10:34:15 -05:00
sethsec-bf 2b19d0ac62 For 1.13, switching default AWS verbosity to 2 (with the exception of all-checks which will be hardocded to a versbosity of 1) 2023-12-26 10:20:04 -05:00
sethsec-bf cef6c9e656 Fixed a bug where iam-simulator custom commands were overwriting the files for the default run. 2023-12-26 10:09:29 -05:00
sethsec-bf 6e146a567a add mfatoken param from recent update to api-gw code 2023-12-26 09:40:25 -05:00
sethsec-bf ae7ed9e19a Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2023-12-24 10:16:36 -05:00
sethsec-bf a690ad3185 bumped version to 1.13.0-prerelease 2023-12-24 10:15:34 -05:00
Seth Art 26cb99778c Merge pull request #64 from wdahlenburg/api-gw
API Gateway Module
2023-12-24 10:13:43 -05:00
sethsec-bf 98af83933e Added intersting functionality to env-vars and workloads 2023-12-23 12:31:51 -05:00
sethsec-bf 82738dbf63 Fixed a bug where when pmapper data was missing iam simulator was not getting IAM roles properly. Also got workloads command working 2023-12-22 10:09:37 -05:00
sethsec-bf 96ab676f49 Feature: First draft of new workloads command - summarizes all workloads 2023-12-21 21:25:05 -05:00
sethsec-bf 83866f46cc Feature: Added a new output file for role trusts that just highlights the overly permissive root trusts without external ID 2023-12-21 21:24:01 -05:00
sethsec-bf 7343852a66 Updated endpoints to use the newly created sdk functions 2023-12-21 11:22:10 -05:00
sethsec-bf b1c248bbe6 Used SDK, added more counters
Moved all api calls to the sdk package.
Added counter to getEndpointsPerAPIGatewayv2 and
getEndpointsPerAPIGateway because in regions with lots of apigateways
this took a long time and it just hung on 66/68 tasks and i worried. The
additional counters will help give visabilty that things are still
moving along.
Added an additional alias api-gws
2023-12-21 10:18:00 -05:00
sethsec-bf 1e2eaf5576 merged from main 2023-12-19 20:15:14 -05:00
sethsec-bf 66ffc57438 go mod tidy 2023-12-19 16:15:13 -05:00
sethsec-bf c4a276e4be graph/neo4j functionali working - detecting cross account attack paths 2023-12-11 18:24:34 -05:00
sethsec-bf 4edbd3326c neo4j cross-account stuff kind of working, pmapper stuff not working 2023-12-06 15:52:49 -05:00
Wyatt Dahlenburg 2ce1f0f82f Codespell 2023-11-16 15:35:13 -07:00
Wyatt Dahlenburg 5eff1c71ac Merging with main + minor updates 2023-11-16 15:05:54 -07:00
sethsec-bf 5c40ef45df initial ideas for graph command 2023-11-09 08:23:37 -05:00
Wyatt Dahlenburg 17011f345c Additional enumeration on API gateways compared to the endpoints module 2023-08-14 21:33:27 -06:00
93 changed files with 11705 additions and 1620 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
@sethsec-bf carlosvendramini-bf
* @bishopfaure @dbravo-bishopfox
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: pip install --user codespell
- run: codespell --ignore-words-list="aks" # --skip="*.css,*.js,*.lock,*.po"
- run: codespell --ignore-words-list="aks" --skip="*.sum"
+6 -1
View File
@@ -15,6 +15,7 @@
# Dependency directories (remove the comment below to include it)
# vendor/
.vscode
.idea
launch.json
######################################################
@@ -70,4 +71,8 @@ cloudfox
*.json
*.csv
*.log
dist/
dist/
# graphvis files
*.gv
*.svg
+41 -6
View File
@@ -14,6 +14,7 @@ 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)
@@ -21,6 +22,10 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments.
## 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)
## 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:
@@ -43,9 +48,9 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
| Provider| CloudFox Commands |
| - | - |
| AWS | 30 |
| AWS | 34 |
| Azure | 4 |
| GCP | Support Planned |
| GCP | 8 |
| Kubernetes | Support Planned |
@@ -62,13 +67,20 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
[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
@@ -97,8 +109,12 @@ Additional policy notes (as of 09/2022):
| - | - | - |
| 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. |
@@ -111,27 +127,46 @@ Additional policy notes (as of 09/2022):
| 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 | [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 | [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 | [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.|
| 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 | [instances](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#instances) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
| Azure | [inventory](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#inventory) | Display an inventory table of all resources per location. |
| Azure | [rbac](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac) | Lists Azure RBAC role assignments at subscription or tenant level |
| Azure | [storage](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#storage) | The storage command is still under development. Currently it only displays limited data about the storage accounts |
| Azure | [vms](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#vms) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
# GCP Commands
| Provider | Command Name | Description
| - | - | - |
| GCP | [whoami](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#whoami) | Display the email address of the GCP authenticated user |
| GCP | [all-checks](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#all-checks) | Runs all available GCP commands |
| GCP | [artifact-registry](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#artifact-registry) | Display GCP artifact registry information |
| GCP | [bigquery](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigquery) | Display Bigquery datasets and tables information |
| GCP | [buckets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#buckets) | Display GCP buckets information |
| GCP | [iam](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#iam) | Display GCP IAM information |
| GCP | [instances](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#instances) | Display GCP Compute Engine instances information |
| GCP | [secrets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#secrets) | Display GCP secrets information |
# Authors
* [Carlos Vendramini](https://github.com/carlosvendramini-bf)
+681
View File
@@ -0,0 +1,681 @@
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
// 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
// }()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("apigateway", 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
}
+60
View File
@@ -0,0 +1,60 @@
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 TestApiGw(t *testing.T) {
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{},
}
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)
}
}
}
+1 -1
View File
@@ -326,7 +326,7 @@ func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool
if err != nil {
return false
}
return publicAccessBlock.IgnorePublicAcls
return aws.ToBool(publicAccessBlock.IgnorePublicAcls) && aws.ToBool(publicAccessBlock.BlockPublicPolicy) && aws.ToBool(publicAccessBlock.RestrictPublicBuckets)
}
+4 -4
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: true,
BlockPublicPolicy: true,
IgnorePublicAcls: false,
RestrictPublicBuckets: true,
BlockPublicAcls: aws.Bool(true),
BlockPublicPolicy: aws.Bool(true),
IgnorePublicAcls: aws.Bool(false),
RestrictPublicBuckets: aws.Bool(true),
},
}
return output, nil
+538
View File
@@ -0,0 +1,538 @@
package aws
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.Copy().BorderStyle(b)
}()
magentaStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("magenta"))
)
type model struct {
preloadedData *AllAccountData
awsAccountsTable table.Model
awsAccountsViewport viewport.Model
mainTable table.Model
mainTableViewport viewport.Model
mainTableData map[int][]table.Row
detailsData map[int]string
detailsViewport viewport.Model // Use viewport for details view
focusSelector int
defaultTableStyle table.Styles
terminalWidth int
terminalHeight int // Add terminal height to manage viewport size
awsSelectedRow int
mainSelectedRow int // Track the currently selected row for detail view updates
keys keyMap
help help.Model
lastKey string
quitting bool
}
type CapeJSON struct {
Account string `json:"account"`
Source string `json:"source"`
Summary string `json:"summary"`
Target string `json:"target"`
IsTargetAdmin string `json:"isTargetAdmin"`
}
type PerAccountData struct {
FilePath string // Path to the JSON file
PrivescPaths []CapeJSON // All records contained in the file
}
type AllAccountData struct {
Files map[string]*PerAccountData // Map of file paths to their records
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.terminalWidth = msg.Width
m.terminalHeight = msg.Height - 7 // Adjust as needed
// Calculate heights based on the specified percentages
awsAccountsHeight := int(float32(m.terminalHeight) * 0.2)
halfHeight := int(float32(m.terminalHeight) * 0.4) // For the other two viewports
// Update dimensions for all viewports
m.awsAccountsViewport.Width = m.terminalWidth - 4
m.awsAccountsViewport.Height = awsAccountsHeight
m.mainTableViewport.Width = m.terminalWidth - 4
m.mainTableViewport.Height = halfHeight
m.awsAccountsTable.SetHeight(awsAccountsHeight - 2)
m.awsAccountsTable.SetWidth(m.terminalWidth - 4)
m.awsAccountsViewport.Width = m.terminalWidth - 4
m.detailsViewport.Width = m.terminalWidth - 4
m.detailsViewport.Height = halfHeight
m.mainTable.SetHeight(halfHeight - 2)
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keys.Up):
m.lastKey = "↑"
case key.Matches(msg, m.keys.Down):
m.lastKey = "↓"
case key.Matches(msg, m.keys.Left):
m.lastKey = "←"
case key.Matches(msg, m.keys.Right):
m.lastKey = "→"
case key.Matches(msg, m.keys.Help):
m.help.ShowAll = !m.help.ShowAll
case key.Matches(msg, m.keys.Tab):
// Switch focus between the the three viewports by cycling focusSelector between 0, 1, and 2
m.focusSelector = (m.focusSelector + 1) % 3
//m.focusOnTable = !m.focusOnTable
// add a case for shift and tab at the same time to to cycle the focusSelector backwards between 0, 1, and 2
case key.Matches(msg, m.keys.ShiftTab):
m.focusSelector = (m.focusSelector - 1) % 3
case key.Matches(msg, m.keys.Quit):
m.quitting = true
return m, tea.Quit
}
//var detailsData map[int]string
//var mainTable table.Model
var err error
var awsCurrentRow int
var mainCurrentRow int
switch m.focusSelector {
case 0:
m.awsAccountsTable, cmd = m.awsAccountsTable.Update(msg)
m.awsAccountsViewport.SetContent(m.awsAccountsTable.View())
awsCurrentRow = m.awsAccountsTable.Cursor()
if m.awsSelectedRow != awsCurrentRow {
m.awsSelectedRow = awsCurrentRow
// Update the viewport content based on the newly selected row
// Load the file for the selected account and show the file content in the main table
m.mainTable, m.detailsData, err = getRecordsForAccount(m.preloadedData.Files[m.awsAccountsTable.Rows()[awsCurrentRow][0]])
if err != nil {
// Handle error
break
}
m.mainTable.SetStyles(s)
m.mainTableViewport.SetContent(m.mainTable.View())
m.detailsViewport.SetContent(m.detailsData[0])
}
case 1:
m.mainTable, cmd = m.mainTable.Update(msg)
// Check if the selected row has changed
m.mainTableViewport.SetContent(m.mainTable.View())
mainCurrentRow = m.mainTable.Cursor()
if m.mainSelectedRow != mainCurrentRow {
m.mainSelectedRow = mainCurrentRow
// Update the viewport content based on the newly selected row
if detail, ok := m.detailsData[m.mainSelectedRow]; ok {
m.detailsViewport.SetContent(detail)
} else {
m.detailsViewport.SetContent(fmt.Sprintf("No details available for main row %d (aws row %d)", m.mainSelectedRow, m.awsSelectedRow))
}
}
case 2:
m.detailsViewport, cmd = m.detailsViewport.Update(msg)
}
}
return m, cmd
}
func (m model) View() string {
// Define styles for active and inactive viewports
activeBorderStyle := lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#00FFFF")). // Cyan for active
Padding(0)
activeBorderStyle.PaddingLeft(1)
activeBorderStyle.PaddingRight(1)
inactiveBorderStyle := lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#808080")). // Gray for inactive
Padding(0)
inactiveBorderStyle.PaddingLeft(1)
inactiveBorderStyle.PaddingRight(1)
// Conditionally apply border styles based on focus
awsTableViewStyle := activeBorderStyle
m.awsAccountsTable.SetStyles(m.defaultTableStyle)
tableViewStyle := inactiveBorderStyle
m.mainTable.SetStyles(m.defaultTableStyle)
detailsViewStyle := inactiveBorderStyle
switch m.focusSelector {
case 0:
awsTableViewStyle = activeBorderStyle
m.awsAccountsTable.SetStyles(m.defaultTableStyle)
tableViewStyle = inactiveBorderStyle
detailsViewStyle = inactiveBorderStyle
case 1:
awsTableViewStyle = inactiveBorderStyle
tableViewStyle = activeBorderStyle
m.mainTable.SetStyles(m.defaultTableStyle)
detailsViewStyle = inactiveBorderStyle
case 2:
awsTableViewStyle = inactiveBorderStyle
tableViewStyle = inactiveBorderStyle
detailsViewStyle = activeBorderStyle
}
// Render the table and details viewports with their styles (adjust based on focus)
awsAccountsView := awsTableViewStyle.Render(m.awsAccountsViewport.View())
tableView := tableViewStyle.Render(m.mainTableViewport.View())
detailsView := detailsViewStyle.Render(m.detailsViewport.View())
// Combine all views
fullView := lipgloss.JoinVertical(lipgloss.Top, awsAccountsView, tableView, detailsView)
helpView := m.help.View(m.keys)
return fmt.Sprintf("%s\n%s", fullView, helpView)
}
func calculateMaxWidths(rows []table.Row) []int {
var maxWidths []int
// make sure the length of the rows is greater than 0
if len(rows) > 0 {
maxWidths = make([]int, len(rows[0]))
} else {
maxWidths = []int{30, 30, 30, 30}
}
for _, row := range rows {
for i, cell := range row {
if len(cell) > maxWidths[i] {
maxWidths[i] = len(cell)
}
}
}
return maxWidths
}
func (m model) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.detailsViewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.detailsViewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func loadFileRecords(filePath string) (*PerAccountData, error) {
// Open the file
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// Decode the JSON data
var records []CapeJSON
decoder := json.NewDecoder(file)
if err := decoder.Decode(&records); err != nil {
return nil, err
}
// Return a new FileRecords instance
return &PerAccountData{
FilePath: filePath,
PrivescPaths: records,
}, nil
}
func preloadData(filePaths []string) (*AllAccountData, error) {
appData := &AllAccountData{
Files: make(map[string]*PerAccountData),
}
for _, filePath := range filePaths {
fileRecords, err := loadFileRecords(filePath)
if err != nil {
return nil, err
}
appData.Files[filePath] = fileRecords
}
return appData, nil
}
func getRecordsForAccount(preloadedData *PerAccountData) (table.Model, map[int]string, error) {
// lets load the records for the first file in the list
records := preloadedData.PrivescPaths
if len(records) < 1 {
mainTable := table.New(
table.WithColumns([]table.Column{
{Title: "Account", Width: 30},
{Title: "Source", Width: 30},
{Title: "Target", Width: 30},
{Title: "isTargetAdmin", Width: 30},
}),
table.WithRows([]table.Row{{"No records found", "", "", ""}}),
table.WithFocused(true),
)
return mainTable, nil, nil
}
// Prepare rows for the table and data for the right view
rows := make([]table.Row, 0, len(records)-1)
detailsData := make(map[int]string) // Initialize the map for the fourth column's data
for i, record := range records[1:] { // Skip the header row
rows = append(rows, table.Row{record.Account, record.Source, record.Target, record.IsTargetAdmin})
detailsData[i] = expandDetailsData(record.Summary)
}
// Calculate max widths
maxWidths := calculateMaxWidths(rows)
// Define columns with calculated widths
columns := make([]table.Column, len(maxWidths))
colNames := []string{}
for _, header := range []string{"Account", "Source", "Target", "isTargetAdmin"} {
colNames = append(colNames, header)
}
for i, width := range maxWidths {
colName := colNames[i]
// if column name width is greater than the calculated width, use the column name width
if len(colName) > width {
width = len(colName)
}
columns[i] = table.Column{
Title: colName,
Width: width,
}
}
mainTable := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
)
return mainTable, detailsData, nil
}
func CapeTUI(outputFiles []string) {
preloadData, err := preloadData(outputFiles)
if err != nil {
fmt.Printf("Error preloading data: %s\n", err)
fmt.Println("Either remove this profile from the list of profiles, or make sure cape can run successfully for this profile")
os.Exit(1)
}
var awsAccountsRows []table.Row
for _, file := range preloadData.Files {
// Extract a user-friendly account name from the file path if needed
// For simplicity, here we use the file path itself
awsAccountsRows = append(awsAccountsRows, table.Row{file.FilePath})
}
mainTable, detailsData, err := getRecordsForAccount(preloadData.Files[outputFiles[0]])
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
awsAccountsWidth := calculateMaxWidths(awsAccountsRows)
awsAccountsTable := table.New(
table.WithColumns([]table.Column{
{Title: "AWS Accounts", Width: awsAccountsWidth[0]},
}),
table.WithRows(awsAccountsRows),
table.WithFocused(true), // Initially unfocused
)
awsAccountsTable.SetStyles(s)
awsAccountsViewport := viewport.New(0, 0) // Initialize with 0 size; will be updated
awsAccountsViewport.SetContent(awsAccountsTable.View()) // Set initial content
mainTable.SetStyles(s)
mainTableViewport := viewport.New(0, 0) // Initialize with 0 size; it will be updated
mainTableViewport.SetContent(mainTable.View()) // Set the initial content of the table viewport
// Initialize viewport for details view
detailsViewportModel := viewport.New(0, 0) // Size will be set based on terminal size in Update
// show the first row's details by default
detailsViewportModel.SetContent(detailsData[0])
m := model{
preloadedData: preloadData,
awsAccountsTable: awsAccountsTable,
awsAccountsViewport: awsAccountsViewport,
mainTable: mainTable,
mainTableViewport: mainTableViewport,
detailsData: detailsData,
detailsViewport: detailsViewportModel,
//focusOnTable: true,
focusSelector: 0,
mainSelectedRow: 0, // Initialize selectedRow with an invalid index
awsSelectedRow: 0,
defaultTableStyle: s,
keys: keys,
help: help.New(),
}
p := tea.NewProgram(m)
if err := p.Start(); err != nil {
fmt.Printf("Error starting program: %s\n", err)
os.Exit(1)
}
}
// keyMap defines a set of keybindings. To work for help it must satisfy
// key.Map. It could also very easily be a map[string]key.Binding.
type keyMap struct {
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Help key.Binding
Quit key.Binding
Tab key.Binding
ShiftTab key.Binding
}
// ShortHelp returns keybindings to be shown in the mini help view. It's part
// of the key.Map interface.
func (k keyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Tab, k.ShiftTab, k.Up, k.Down, k.Help, k.Quit}
}
// FullHelp returns keybindings for the expanded help view. It's part of the
// key.Map interface.
func (k keyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Up, k.Down, k.Left, k.Right}, // first column
{k.Tab, k.ShiftTab, k.Help, k.Quit}, // second column
}
}
var keys = keyMap{
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "move up"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "move down"),
),
Left: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("←/h", "move left"),
),
Right: key.NewBinding(
key.WithKeys("right", "l"),
key.WithHelp("→/l", "move right"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("?", "toggle help"),
),
Quit: key.NewBinding(
key.WithKeys("q", "esc", "ctrl+c"),
key.WithHelp("q", "quit"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "Switch window focus"),
),
ShiftTab: key.NewBinding(
key.WithKeys("shift+tab"),
key.WithHelp("shift+tab", "Switch window focus"),
),
}
func expandDetailsData(input string) string {
output := ""
lines := strings.Split(input, "\n")
for _, line := range lines {
var hop, option int
var rest string
// Use Sscanf to extract hop and option values
n, err := fmt.Sscanf(line, "[Hop: %d] [Option: %d]", &hop, &option)
if err != nil || n != 2 {
fmt.Printf("Error parsing line, expected 2 got %d: %v\n", n, err)
continue
}
// Extract the rest of the line manually
restIndex := strings.Index(line, "]") + 1
restIndex = strings.Index(line[restIndex:], "]") + restIndex + 1
if restIndex > 1 && restIndex < len(line) {
rest = line[restIndex+1:] // +1 to skip the space after the second ]
}
// Construct the formatted output
formattedLine := fmt.Sprintf("[Hop: %d][Option: %d]\n\t%s\n\n", hop, option, rest)
output += formattedLine
}
return output
}
+981
View File
@@ -0,0 +1,981 @@
package aws
import (
"fmt"
"path/filepath"
"strings"
"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"
"github.com/bishopfox/knownawsaccountslookup"
"github.com/dominikbraun/graph"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type CapeCommand struct {
// General configuration data
Cmd cobra.Command
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
GlobalGraph graph.Graph[string, string]
PmapperDataBasePath string
AnalyzedAccounts map[string]CapeJobInfo
CapeAdminOnly bool
AccountsNotAnalyzed []string
output internal.OutputData2
modLog *logrus.Entry
}
type CapeJobInfo struct {
AccountID string
Profile string
AnalyzedSuccessfully bool
AdminOnlyAnalysis bool
Source string
}
func (m *CapeCommand) RunCapeCommand() {
// These struct values are used by the output module
m.output.Verbosity = m.Verbosity
m.output.Directory = m.AWSOutputDirectory
m.output.CallingModule = "cape"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.output.FilePath = filepath.Join(m.AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: m.Verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(m.AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
// Table #1: Inbound Privilege Escalation Paths
fmt.Printf("[%s][%s] Printing inbound privesc paths for account: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
if !m.CapeAdminOnly {
fmt.Printf("[%s][%s] This can take a really long time if the number of vertices/edges is in the thousands. Consider stopping here and re-running cape with --admin-only to speed this step up!\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
} else {
fmt.Printf("[%s][%s] This can take a really long time if the number of vertices/edges is in the thousands.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
header, body, _ := m.generateInboundPrivEscTableData()
var fileName string
if m.CapeAdminOnly {
fileName = "inbound-privesc-paths-admin-targets-only"
} else {
fileName = "inbound-privesc-paths-all-targets"
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: header,
Body: body,
//TableCols: tableCols,
Name: fileName,
SkipPrintToScreen: false,
})
// // Table #2: Outbound Privilege Escalation Paths
// header, body, tableCols := m.generateOutBoundPrivEscTable()
// o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
// Header: header,
// Body: body,
// TableCols: tableCols,
// Name: "outbound-privesc-paths",
// SkipPrintToScreen: false,
// })
o.WriteFullOutput(o.Table.TableFiles, nil)
fmt.Println("The following accounts are trusted by this account, but were not analyzed as part of this run.")
fmt.Println("As a result, we cannot determine which principals in these accounts have permission to assume roles in this account.")
// for account := range m.AnalyzedAccounts {
// if m.AnalyzedAccounts[account].AnalyzedSuccessfully == false {
// fmt.Println("\t\t" + account)
// }
// }
for _, account := range m.AccountsNotAnalyzed {
fmt.Println("\t\t" + account)
}
}
func (m *CapeCommand) generateInboundPrivEscTableData() ([]string, [][]string, []string) {
var body [][]string
var tableCols []string
var header []string
header = []string{
"Account",
"Source",
"Target",
"isTargetAdmin",
"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.
// 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",
"Source",
"Target",
"isTargetAdmin",
"Summary",
}
// Otherwise, use these columns.
} else {
tableCols = []string{
"Source",
"Target",
"isTargetAdmin",
"Summary",
}
}
var privescPathsBody [][]string
allGlobalNodes, _ := m.GlobalGraph.AdjacencyMap()
for destination := range allGlobalNodes {
d, destinationVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(destination)
// if the user specified the CapeAdminOnly flag, then we only want to show paths to admin roles
if m.CapeAdminOnly {
// if the user specified the CapeAdminOnly flag, then we only want to show paths to admin roles
if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" {
//for the destination vertex, we only want to deal with the ones that are in this account
if destinationVertexWithProperties.Attributes["AccountID"] == aws.ToString(m.Caller.Account) {
privescPathsBody = m.findPathsToThisDestination(allGlobalNodes, d, destinationVertexWithProperties)
body = append(body, privescPathsBody...)
}
}
} else {
//for the destination vertex, we only want to deal with the ones that are in this account
if destinationVertexWithProperties.Attributes["AccountID"] == aws.ToString(m.Caller.Account) {
privescPathsBody := m.findPathsToThisDestination(allGlobalNodes, d, destinationVertexWithProperties)
body = append(body, privescPathsBody...)
}
}
}
body = append(body, privescPathsBody...)
return header, body, tableCols
}
func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[string]graph.Edge[string], d string, destinationVertexWithProperties graph.VertexProperties) [][]string {
var privescPathsBody [][]string
var paths string
// now let's look at every other vertex and see if it has a path to this destination
for source := range allGlobalNodes {
s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source)
//for the source vertex, we only want to deal with the ones that are NOT in this account
if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) {
// now let's see if there is a path from this source to our destination
path, _ := graph.ShortestPath(m.GlobalGraph, s, d)
// if we have a path, then lets document this source as having a path to our destination
if path != nil {
if s != d {
fmt.Printf("[%s][%s] Found a path from %s to %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), s, d)
// check to see if the source account was analyzed. If not, lets add it to the list of accounts that were not analyzed
if strings.Contains(s, "Not analyzed/in-scope") {
// add it to the m.AccountsNotAnalyzed if it doesn't already exist
if !internal.Contains(s, m.AccountsNotAnalyzed) {
m.AccountsNotAnalyzed = append(m.AccountsNotAnalyzed, s)
}
}
paths = ""
// if we got here there's a path. Lets print the reason and the short reason for each edge in the path to the screen
// and then lets print the full path to the screen
for i := 0; i < len(path)-1; i++ {
thisEdge, _ := m.GlobalGraph.Edge(path[i], path[i+1])
j := 0
for _, value := range thisEdge.Properties.Attributes {
value = strings.ReplaceAll(value, ",", " and")
paths += fmt.Sprintf("[Hop: %d] [Option: %d] [%s] [%s] [%s]\n", i, j, thisEdge.Source, value, thisEdge.Target)
j++
}
}
//trim the last newline from csvPaths
paths = strings.TrimSuffix(paths, "\n")
if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" {
privescPathsBody = append(privescPathsBody, []string{
aws.ToString(m.Caller.Account),
s,
magenta(d),
magenta(destinationVertexWithProperties.Attributes["IsAdminString"]),
paths})
} else {
privescPathsBody = append(privescPathsBody, []string{
aws.ToString(m.Caller.Account),
s,
d,
destinationVertexWithProperties.Attributes["IsAdminString"],
paths})
}
}
}
}
}
return privescPathsBody
}
func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendors, analyzedAccounts map[string]CapeJobInfo) Node {
//var isAdmin, canPrivEscToAdmin string
accountId := strings.Split(aws.ToString(role.Arn), ":")[4]
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
if err != nil {
internal.TxtLog.Error(err.Error())
return Node{}
}
var TrustedPrincipals []TrustedPrincipal
var TrustedServices []TrustedService
var TrustedFederatedProviders []TrustedFederatedProvider
//var TrustedFederatedSubjects string
var trustedProvider string
var trustedSubjects []string
var vendorName string
var isAnalyzedAccount bool
for _, statement := range trustsdoc.Statement {
for _, principal := range statement.Principal.AWS {
if strings.Contains(principal, ":root") {
//check to see if the trustedRootAccountID is known
trustedRootAccountID := strings.Split(principal, ":")[4]
vendorName = vendors.GetVendorNameFromAccountID(trustedRootAccountID)
// check to see if trustedRootAccountID is in the m.AnalyzedAccounts map
if _, ok := analyzedAccounts[trustedRootAccountID]; ok {
isAnalyzedAccount = analyzedAccounts[trustedRootAccountID].AnalyzedSuccessfully
} else {
isAnalyzedAccount = false
}
}
TrustedPrincipals = append(TrustedPrincipals, TrustedPrincipal{
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
VendorName: vendorName,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
AccountIsInAnalyzedAccountList: isAnalyzedAccount,
})
}
for _, service := range statement.Principal.Service {
TrustedServices = append(TrustedServices, TrustedService{
TrustedService: service,
AccountID: accountId,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
})
}
for _, federated := range statement.Principal.Federated {
// provider accountID
//accountId := strings.Split(federated, ":")[4]
trustedProvider, trustedSubjects = parseFederatedTrustPolicy(statement)
TrustedFederatedProviders = append(TrustedFederatedProviders, TrustedFederatedProvider{
TrustedFederatedProvider: federated,
ProviderShortName: trustedProvider,
//ProviderAccountId: accountId,
TrustedSubjects: trustedSubjects,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
})
}
}
node := Node{
Arn: aws.ToString(role.Arn),
Type: "Role",
AccountID: accountId,
Name: aws.ToString(role.RoleName),
TrustsDoc: trustsdoc,
TrustedPrincipals: TrustedPrincipals,
TrustedServices: TrustedServices,
TrustedFederatedProviders: TrustedFederatedProviders,
}
return node
}
func ConvertIAMUserToNode(user types.User) Node {
accountId := strings.Split(aws.ToString(user.Arn), ":")[4]
node := Node{
Arn: aws.ToString(user.Arn),
Type: "User",
AccountID: accountId,
Name: aws.ToString(user.UserName),
}
return node
}
func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) []Node {
var newNodes []Node
// 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)
}
// 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)) {
// newNodes = append(newNodes, Node{
// Arn: a.Arn,
// Type: "Account",
// AccountID: a.AccountID,
// Name: a.Name,
// })
// } else
if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" {
// First lets take care of vendor accounts
newNodes = append(newNodes, Node{
Arn: fmt.Sprintf("%s [%s]", TrustedPrincipal.VendorName, TrustedPrincipal.TrustedPrincipal),
//Arn: TrustedPrincipal.VendorName,
Type: "Account",
AccountID: trustedPrincipalAccount,
Name: TrustedPrincipal.VendorName,
VendorName: TrustedPrincipal.VendorName,
})
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && !TrustedPrincipal.AccountIsInAnalyzedAccountList {
// Next lets take care of accounts that are not in the analyzed account list and add the full :root as the node
newNodes = append(newNodes, Node{
Arn: fmt.Sprintf("%s [Not analyzed/in-scope]", TrustedPrincipal.TrustedPrincipal),
Type: "Account",
AccountID: trustedPrincipalAccount,
})
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName == "" {
// Now with those out of the way, lets take care of the accounts that are in the analyzed account list
newNodes = append(newNodes, Node{
Arn: TrustedPrincipal.TrustedPrincipal,
Type: "Account",
AccountID: trustedPrincipalAccount,
})
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":user")) {
newNodes = append(newNodes, Node{
Arn: TrustedPrincipal.TrustedPrincipal,
Type: "User",
AccountID: trustedPrincipalAccount,
})
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":role")) {
newNodes = append(newNodes, Node{
Arn: TrustedPrincipal.TrustedPrincipal,
Type: "Role",
AccountID: trustedPrincipalAccount,
})
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":group")) {
newNodes = append(newNodes, Node{
Arn: TrustedPrincipal.TrustedPrincipal,
Type: "Group",
AccountID: trustedPrincipalAccount,
})
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":assumed-role")) {
newNodes = append(newNodes, Node{
Arn: TrustedPrincipal.TrustedPrincipal,
Type: "AssumedRole",
AccountID: trustedPrincipalAccount,
})
}
}
// 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
var providerAndSubject string
for _, trustedSubject := range TrustedFederatedProvider.TrustedSubjects {
if trustedSubject == "Not applicable" {
providerAndSubject = TrustedFederatedProvider.ProviderShortName
} else {
//providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject
providerAndSubject = fmt.Sprintf("%s [%s]", TrustedFederatedProvider.ProviderShortName, trustedSubject)
}
//fmt.Println("TrustedFederatedProvider: ", TrustedFederatedProvider.TrustedFederatedProvider)
// if the TrustedFederatedProvider.TrustedFederatedProvider is an arn (check to see if it has at least 4 semicolons), grab the account id. Otherwise, use a.AccountID
var accountID string
if strings.Count(TrustedFederatedProvider.TrustedFederatedProvider, ":") >= 4 {
accountID = strings.Split(TrustedFederatedProvider.TrustedFederatedProvider, ":")[4]
} else {
accountID = a.AccountID
}
newNodes = append(newNodes, Node{
Arn: providerAndSubject,
Name: TrustedFederatedProvider.ProviderShortName,
Type: "FederatedIdentity",
AccountID: accountID,
})
}
}
return newNodes
}
func MergeNodes(nodes []Node) []Node {
nodeMap := make(map[string]Node)
for _, node := range nodes {
existingNode, exists := nodeMap[node.Arn]
if exists {
// fmt.Println("Found a duplicate node: %s, merging", node.Arn)
// fmt.Println("Existing node: %v", existingNode)
// fmt.Println("New node: %v", node)
mergedNode := mergeNodeData(existingNode, node)
nodeMap[node.Arn] = mergedNode
//fmt.Println("Merged node: %v", mergedNode)
} else {
nodeMap[node.Arn] = node
}
}
var mergedNodes []Node
for _, node := range nodeMap {
mergedNodes = append(mergedNodes, node)
}
return mergedNodes
}
func mergeNodeData(existingNode Node, newNode Node) Node {
if existingNode.Arn == "" {
existingNode.Arn = newNode.Arn
} else {
existingNode.Arn = existingNode.Arn
}
if existingNode.Name == "" {
existingNode.Name = newNode.Name
} else {
existingNode.Name = existingNode.Name
}
if existingNode.Type == "" {
existingNode.Type = newNode.Type
} else {
existingNode.Type = existingNode.Type
}
if existingNode.AccountID == "" {
existingNode.AccountID = newNode.AccountID
} else {
existingNode.AccountID = existingNode.AccountID
}
if existingNode.CanPrivEscToAdminString == "" {
existingNode.CanPrivEscToAdminString = newNode.CanPrivEscToAdminString
} else {
existingNode.CanPrivEscToAdminString = existingNode.CanPrivEscToAdminString
}
if existingNode.IsAdminString == "" {
existingNode.IsAdminString = newNode.IsAdminString
} else {
existingNode.IsAdminString = existingNode.IsAdminString
}
if existingNode.VendorName == "" {
existingNode.VendorName = newNode.VendorName
} else {
existingNode.VendorName = existingNode.VendorName
}
if existingNode.AccessKeys == 0 {
existingNode.AccessKeys = newNode.AccessKeys
} else {
existingNode.AccessKeys = existingNode.AccessKeys
}
if existingNode.ActivePassword == false {
existingNode.ActivePassword = newNode.ActivePassword
} else {
existingNode.ActivePassword = existingNode.ActivePassword
}
if existingNode.HasMfa == false {
existingNode.HasMfa = newNode.HasMfa
} else {
existingNode.HasMfa = existingNode.HasMfa
}
if existingNode.PathToAdmin == false {
existingNode.PathToAdmin = newNode.PathToAdmin
} else {
existingNode.PathToAdmin = existingNode.PathToAdmin
}
if existingNode.AttachedPolicies == nil {
existingNode.AttachedPolicies = newNode.AttachedPolicies
} else {
existingNode.AttachedPolicies = existingNode.AttachedPolicies
}
if existingNode.TrustedFederatedProviders == nil {
existingNode.TrustedFederatedProviders = newNode.TrustedFederatedProviders
} else {
existingNode.TrustedFederatedProviders = existingNode.TrustedFederatedProviders
}
if existingNode.TrustedPrincipals == nil {
existingNode.TrustedPrincipals = newNode.TrustedPrincipals
} else {
existingNode.TrustedPrincipals = existingNode.TrustedPrincipals
}
if existingNode.TrustedServices == nil {
existingNode.TrustedServices = newNode.TrustedServices
} else {
existingNode.TrustedServices = existingNode.TrustedServices
}
// if existingNode.TrustsDoc.Statement == nil {
// existingNode.TrustsDoc = newNode.TrustsDoc
// } else {
// }
return existingNode
}
func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
// 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.Arn,
//graph.EdgeAttribute("AssumeRole", "Same account explicit trust"),
graph.EdgeAttribute("AssumeRole", "can assume (because of an explicit same account trust) "),
)
if err != nil {
//fmt.Println(err)
//fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Same account explicit trust")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
//fmt.Println("Edge already exists")
// get the existing edge
existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes["AssumeRole"] = "can assume (because of an explicit same account trust) "
err = GlobalGraph.UpdateEdge(
TrustedPrincipal.TrustedPrincipal,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}
}
}
// If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role
// if the role we are looking at trusts root in it's own account
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 policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.Arn) {
// make a CAN_ASSUME relationship between the trusted principal and this role
//evaluate if the principal is a user or a role and set a variable accordingly
//var principalType schema.NodeLabel
if strings.EqualFold(PermissionsRow.Type, "User") || strings.EqualFold(PermissionsRow.Type, "Role") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Arn,
//graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"),
graph.EdgeAttribute("AssumeRole", "can assume (because of a same account root trust and trusted principal has permission to assume role) "),
)
if err != nil {
// fmt.Println(err)
// fmt.Println(PermissionsRow.Arn + a.Arn + "Same account root trust and trusted principal has permission to assume role")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
// get the existing edge
existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes["AssumeRole"] = "can assume (because of a same account root trust and trusted principal has permission to assume role) "
err = GlobalGraph.UpdateEdge(
PermissionsRow.Arn,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}
}
}
}
}
if strings.EqualFold(PermissionsRow.Effect, "Deny") {
// if the action is deny, we need to remove any edges between PermissionsRow.Arn and a.Arn
// if the edge exists, remove it
err := GlobalGraph.RemoveEdge(PermissionsRow.Arn, a.Arn)
if err != nil {
fmt.Println(err)
}
}
}
}
}
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" {
// 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
err := GlobalGraph.AddEdge(
//TrustedPrincipal.TrustedPrincipal,
//TrustedPrincipal.VendorName,
fmt.Sprintf("%s [%s]", TrustedPrincipal.VendorName, TrustedPrincipal.TrustedPrincipal),
a.Arn,
//graph.EdgeAttribute("VendorAssumeRole", "Cross account root trust and trusted principal is a vendor"),
graph.EdgeAttribute("VendorAssumeRole", "can assume (because of a cross account root trust and trusted principal is a vendor) "),
)
if err != nil {
// fmt.Println(err)
// fmt.Println(TrustedPrincipal.VendorName + a.Arn + "Cross account root trust and trusted principal is a vendor")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
// get the existing edge
existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.VendorName, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes["VendorAssumeRole"] = "can assume (because of a cross account root trust and trusted principal is a vendor) "
err := GlobalGraph.UpdateEdge(
//fmt.Sprintf("%s-%s", a.Arn, TrustedPrincipal.VendorName),
TrustedPrincipal.TrustedPrincipal,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}
}
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) && !TrustedPrincipal.AccountIsInAnalyzedAccountList {
// first lets check to see if the trustedRootAccountID is in the map of analzyeddAccounts
// if it is not, we can't iterate over the permissions, so we will just have to create an edge :root princpal and this role
err := GlobalGraph.AddEdge(
//TrustedPrincipal.TrustedPrincipal,
fmt.Sprintf("%s [Not analyzed/in-scope]", TrustedPrincipal.TrustedPrincipal),
a.Arn,
//graph.EdgeAttribute("CrossAccountRootTrust", "Cross account root trust and trusted principal is not in the analyzed account list"),
graph.EdgeAttribute("CrossAccountRootTrust", "can assume (because of a cross account root trust and trusted principal is not in the analyzed account list) "),
)
if err != nil {
// fmt.Println(err)
// fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Cross account root trust and trusted principal is not in the analyzed account list")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
// get the existing edge
existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes["CrossAccountRootTrust"] = "can assume (because of a cross account root trust and trusted principal is not in the analyzed account list) "
err := GlobalGraph.UpdateEdge(
TrustedPrincipal.TrustedPrincipal,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}
}
} 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 policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") {
// 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.Arn,
//graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"),
graph.EdgeAttribute("CrossAccountAssumeRole", "can assume (because of a cross account root trust and trusted principal has permission to assume role) "),
)
if err != nil {
//fmt.Println(err)
//fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
// get the existing edge
existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes["CrossAccountAssumeRole"] = "can assume (because of a cross account root trust and trusted principal has permission to assume role) "
err = GlobalGraph.UpdateEdge(
PermissionsRow.Arn,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}
}
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Arn,
//graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"),
graph.EdgeAttribute("CrossAccountAssumeRole", "can assume (because of a cross account root trust and trusted principal has permission to assume role) "),
)
if err != nil {
//fmt.Println(err)
//fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
// get the existing edge
existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes["CrossAccountAssumeRole"] = "can assume (because of a cross account root trust and trusted principal has permission to assume role) "
err = GlobalGraph.UpdateEdge(
PermissionsRow.Arn,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}
}
}
}
}
if strings.EqualFold(PermissionsRow.Effect, "Deny") {
// if the action is deny, we need to remove any edges between PermissionsRow.Arn and a.Arn
// if the edge exists, remove it
err := GlobalGraph.RemoveEdge(PermissionsRow.Arn, a.Arn)
if err != nil {
fmt.Println(err)
}
}
}
}
}
}
}
// 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
var providerAndSubject string
for _, trustedSubject := range TrustedFederatedProvider.TrustedSubjects {
if trustedSubject == "Not applicable" {
providerAndSubject = TrustedFederatedProvider.ProviderShortName
} else {
//providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject
providerAndSubject = fmt.Sprintf("%s [%s]", TrustedFederatedProvider.ProviderShortName, trustedSubject)
}
err := GlobalGraph.AddEdge(
providerAndSubject,
a.Arn,
//graph.EdgeAttribute("FederatedAssumeRole", "Trusted federated provider"),
graph.EdgeAttribute("FederatedAssumeRole", "can assume (because of a trusted federated provider) "),
)
if err != nil {
//fmt.Println(err)
//fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Arn + "Trusted federated provider")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
// get the existing edge
existingEdge, _ := GlobalGraph.Edge(TrustedFederatedProvider.TrustedFederatedProvider, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes["FederatedAssumeRole"] = "can assume (because of a trusted federated provider) "
err = GlobalGraph.UpdateEdge(
providerAndSubject,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}
}
}
}
}
+27 -6
View File
@@ -3,11 +3,13 @@ package aws
import (
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codebuild"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/efs"
"github.com/aws/aws-sdk-go-v2/service/fsx"
"github.com/aws/aws-sdk-go-v2/service/glue"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/lambda"
"github.com/aws/aws-sdk-go-v2/service/organizations"
"github.com/aws/aws-sdk-go-v2/service/s3"
@@ -17,13 +19,13 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sts"
)
func initIAMSimClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
func InitIamCommandClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
iamSimMod := IamSimulatorModule{
IAMClient: iamSimPPClient,
Caller: caller,
AWSProfile: AWSProfile,
Goroutines: Goroutines,
IAMClient: iamSimPPClient,
Caller: caller,
AWSProfileProvided: AWSProfile,
Goroutines: Goroutines,
}
return iamSimMod
@@ -116,7 +118,7 @@ func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string
return fileSystemsClient
}
func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) OrgModule {
func InitOrgsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) OrgModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
orgClient := OrgModule{
OrganizationsClient: organizations.NewFromConfig(AWSConfig),
@@ -127,6 +129,17 @@ func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVers
return orgClient
}
func InitPermissionsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) IamPermissionsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
permissionsClient := IamPermissionsModule{
IAMClient: iam.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
}
return permissionsClient
}
func InitSecretsManagerClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) *secretsmanager.Client {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
return secretsmanager.NewFromConfig(AWSConfig)
@@ -136,3 +149,11 @@ func InitGlueClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVer
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
return glue.NewFromConfig(AWSConfig)
}
func InitOrgClient(AWSConfig aws.Config) *organizations.Client {
return organizations.NewFromConfig(AWSConfig)
}
func InitIAMClient(AWSConfig aws.Config) *iam.Client {
return iam.NewFromConfig(AWSConfig)
}
+7 -6
View File
@@ -19,10 +19,11 @@ type CodeBuildModule struct {
CodeBuildClient sdk.CodeBuildClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Goroutines int
AWSProfile string
@@ -64,8 +65,8 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi
}
fmt.Printf("[%s][%s] Enumerating CodeBuild projects for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
+146 -135
View File
@@ -3,7 +3,6 @@ package aws
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
@@ -22,7 +21,6 @@ type DatabasesModule struct {
RDSClient sdk.RDSClientInterface
RedshiftClient sdk.AWSRedShiftClientInterface
DynamoDBClient sdk.DynamoDBClientInterface
DocDBClient sdk.DocDBClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
@@ -53,6 +51,7 @@ type Database struct {
Protocol string
Public string
Size string
Roles string
}
func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int) {
@@ -68,13 +67,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, DynamoDB\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
fmt.Printf("[%s][%s] Supported Services: RDS, Redshift, DynamoDB, DocumentDB, Neptune\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 the task status spinner/updated
//fire up the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
@@ -112,7 +111,8 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
"Size",
"UserName",
"Endpoint",
//"Port",
"Port",
"Roles",
//"Protocol",
//"Public",
@@ -139,6 +139,8 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
"Size",
"UserName",
"Endpoint",
"Port",
"Roles",
}
// Otherwise, use the default columns.
} else {
@@ -150,6 +152,7 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
"Size",
"UserName",
"Endpoint",
"Port",
}
}
@@ -166,7 +169,8 @@ 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)),
strconv.Itoa(int(m.Databases[i].Port)),
m.Databases[i].Roles,
// m.Databases[i].Protocol,
// m.Databases[i].Public,
},
@@ -174,13 +178,17 @@ func (m *DatabasesModule) PrintDatabases(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)))
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,
Wrap: m.WrapTable,
DirectoryName: filepath,
},
Loot: internal.LootClient{
DirectoryName: filepath,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
@@ -190,9 +198,15 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
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.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
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)
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))
@@ -217,56 +231,74 @@ func (m *DatabasesModule) Receiver(receiver chan Database, receiverDone chan boo
func (m *DatabasesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
serviceMap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("rds", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
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)
}
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)
}
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
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)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
panic(err.Error())
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
check.wg.Add(1)
go check.executor(check.region, check.wg, check.semaphore, check.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,
})
}
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 {
path := filepath.Join(outputDirectory, "loot")
f := filepath.Join(path, "databases-UrlsOnly.txt")
var out string
@@ -275,12 +307,12 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
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()
@@ -291,6 +323,8 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
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) {
@@ -307,45 +341,58 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
m.CommandCounter.Pending--
m.CommandCounter.Executing++
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
DBClusters, err := sdk.CachedRDSDescribeDBClusters(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 {
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 aws.ToBool(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: aws.ToInt32(port),
Protocol: aws.ToString(instance.Engine),
Public: public,
}
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) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
@@ -369,7 +416,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++
@@ -381,19 +428,19 @@ 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) {
public = "True"
} else {
public = "False"
}
port := cluster.Endpoint.Port
dataReceiver <- Database{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: aws.ToInt32(port),
Port: port,
Protocol: protocol,
Public: public,
}
@@ -442,48 +489,12 @@ func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGrou
Endpoint: "N/A",
}
}
}
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 isNeptune(engine *string) bool {
return *engine == "neptune"
}
}()
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)),
}
}
func isDocDB(engine *string) bool {
return *engine == "docdb"
}
+57
View File
@@ -0,0 +1,57 @@
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",
"db2.cluster-123456789012.us-west-2.rds.amazonaws.com",
"db3.cluster-123456789012.us-west-2.neptune.amazonaws.com",
"db4.cluster-123456789012.us-west-2.docdb.amazonaws.com",
}
for _, expected := range expectedResults {
if !strings.Contains(string(resultsFile), expected) {
t.Errorf("Expected %s to be in the results file", expected)
}
}
}
+276
View File
@@ -0,0 +1,276 @@
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
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()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("clouddirectory", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getDirectoriesPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *DirectoryModule) Receiver(receiver chan Directory, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Directories = append(m.Directories, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *DirectoryModule) getDirectoriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// Get directories
directories, err := sdk.CachedDSDescribeDirectories(m.DSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err)
}
for _, directory := range directories {
trusts, err := sdk.CachedDSDescribeTrusts(m.DSClient, aws.ToString(m.Caller.Account), r, *directory.DirectoryId)
if err != nil {
m.modLog.Error(err)
}
dataReceiver <- Directory{
DirectoryId: *directory.DirectoryId,
DNS: *directory.Name,
NetBios: *directory.ShortName,
Region: r,
AccessURL: *directory.AccessUrl,
Alias: *directory.Alias,
OsVersion: fmt.Sprintf("%s", directory.OsVersion),
TrustInfo: m.formatTrusts(trusts),
}
}
}
func (m *DirectoryModule) formatTrusts(t []dsTypes.Trust) string {
var output string = ""
for idx, trust := range t {
if idx != 0 {
output = output + "\n"
}
if trust.TrustDirection == "One-Way: Outgoing" {
output = output + "→"
} else if trust.TrustDirection == "One-Way: Ingoing" {
output = output + "←"
} else {
output = output + "↔"
}
output = fmt.Sprintf("%s %s", output, *trust.RemoteDomainName)
// check trust type (external or forest)
if trust.TrustType == "External" {
output = fmt.Sprintf("%s (%s)", output, "Domain")
} else {
output = fmt.Sprintf("%s (%s)", output, "Forest")
}
}
return output
}
+7 -6
View File
@@ -25,10 +25,11 @@ type ECSTasksModule struct {
EC2Client sdk.AWSEC2ClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
AWSProfile string
Goroutines int
@@ -73,8 +74,8 @@ func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) {
fmt.Printf("[%s][%s] Enumerating ECS tasks in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
+7 -6
View File
@@ -22,10 +22,11 @@ type EKSModule struct {
EKSClient sdk.EKSClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Goroutines int
AWSProfile string
@@ -72,8 +73,8 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
fmt.Printf("[%s][%s] Enumerating EKS clusters for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
+149 -290
View File
@@ -930,10 +930,6 @@ 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)
@@ -943,16 +939,20 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
return
}
for {
GetDomainNames, err := m.APIGatewayClient.GetDomainNames(
context.TODO(),
&apigateway.GetDomainNamesInput{
Position: PaginationControl2,
},
func(o *apigateway.Options) {
o.Region = r
},
)
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)
if err != nil {
if errors.As(err, &oe) {
@@ -963,82 +963,43 @@ func (m *EndpointsModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGrou
break
}
for _, item := range GetDomainNames.Items {
for _, mapping := range GetBasePathMappings {
stage := aws.ToString(mapping.Stage)
basePath := aws.ToString(mapping.BasePath)
if basePath == "(none)" {
basePath = "" // Empty string since '/' is already prepended
}
domain := aws.ToString(item.DomainName)
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)
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
}
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)
}
break
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
}
}
}
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
@@ -1056,15 +1017,7 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
public = "True"
}
GetStages, err := m.APIGatewayClient.GetStages(
context.TODO(),
&apigateway.GetStagesInput{
RestApiId: &id,
},
func(o *apigateway.Options) {
o.Region = r
},
)
GetStages, err := sdk.CachedApiGatewayGetStages(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
if errors.As(err, &oe) {
@@ -1074,54 +1027,38 @@ func (m *EndpointsModule) getEndpointsPerAPIGateway(r string, api apigatewayType
m.CommandCounter.Error++
}
for {
GetResources, err := m.APIGatewayClient.GetResources(
context.TODO(),
&apigateway.GetResourcesInput{
RestApiId: &id,
Position: PaginationControl2,
},
func(o *apigateway.Options) {
o.Region = r
},
)
GetResources, err := sdk.CachedApiGatewayGetResources(m.APIGatewayClient, 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++
break
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.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", raw_endpoint, stageName, path)
for _, stage := range GetStages.Item {
stageName := aws.ToString(stage.StageName)
for _, resource := range GetResources {
if len(resource.ResourceMethods) != 0 {
path := aws.ToString(resource.Path)
endpoints = append(endpoints, Endpoint{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
Protocol: protocol,
Public: public,
})
}
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,
})
}
}
if GetResources.Position != nil {
PaginationControl2 = GetResources.Position
} else {
PaginationControl2 = nil
break
}
}
return endpoints
}
@@ -1170,10 +1107,6 @@ 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)
@@ -1183,16 +1116,20 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
return
}
for {
GetDomainNames, err := m.APIGatewayv2Client.GetDomainNames(
context.TODO(),
&apigatewayv2.GetDomainNamesInput{
NextToken: PaginationControl2,
},
func(o *apigatewayv2.Options) {
o.Region = r
},
)
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)
if err != nil {
if errors.As(err, &oe) {
@@ -1203,87 +1140,46 @@ func (m *EndpointsModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGr
break
}
for _, item := range GetDomainNames.Items {
for _, mapping := range GetApiMappings {
stage := aws.ToString(mapping.Stage)
if stage == "$default" {
stage = ""
}
path := aws.ToString(mapping.ApiMappingKey)
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
}
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)
}
break
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
endpoint.Name = domain
dataReceiver <- endpoint
}
}
}
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
@@ -1295,98 +1191,61 @@ func (m *EndpointsModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2
protocol := "https"
var stages []string
for {
GetStages, err := m.APIGatewayv2Client.GetStages(
context.TODO(),
&apigatewayv2.GetStagesInput{
ApiId: &id,
NextToken: PaginationControl2,
},
func(o *apigatewayv2.Options) {
o.Region = r
},
)
GetStages, err := sdk.CachedAPIGatewayv2GetStages(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++
break
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.Items {
s := aws.ToString(stage.StageName)
if s == "$default" {
s = ""
}
stages = append(stages, s)
for _, stage := range GetStages {
s := aws.ToString(stage.StageName)
if s == "$default" {
s = ""
}
stages = append(stages, s)
}
if GetStages.NextToken != nil {
PaginationControl2 = GetStages.NextToken
} else {
PaginationControl2 = nil
break
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]
}
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,
})
}
}
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
}
+75 -12
View File
@@ -66,6 +66,7 @@ type EnvironmentVariable struct {
region string
environmentVarName string
environmentVarValue string
interesting bool
}
func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
@@ -161,19 +162,35 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
}
}
m.determineIfInteresting()
//Table rows
for _, envVar := range m.EnvironmentVariables {
m.output.Body = append(
m.output.Body, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
envVar.name,
envVar.environmentVarName,
envVar.environmentVarValue,
},
)
if envVar.interesting {
m.output.Body = append(
m.output.Body, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
envVar.name,
magenta(envVar.environmentVarName),
magenta(envVar.environmentVarValue),
},
)
} else {
m.output.Body = append(
m.output.Body, []string{
aws.ToString(m.Caller.Account),
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)))
@@ -185,14 +202,27 @@ func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
Wrap: m.WrapTable,
},
}
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,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
// 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)))
@@ -202,6 +232,20 @@ 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 {
@@ -786,3 +830,22 @@ 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
}
+418
View File
@@ -0,0 +1,418 @@
package aws
import (
"context"
"fmt"
"os"
"strings"
ingestor "github.com/BishopFox/cloudfox/aws/graph/ingester"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/knownawsaccountslookup"
"github.com/sirupsen/logrus"
)
type GraphCommand struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
AWSProfile string
WrapTable bool
AWSOutputType string
AWSTableCols string
Verbosity int
AWSOutputDirectory string
AWSConfig aws.Config
Version string
SkipAdminCheck bool
PmapperDataBasePath string
pmapperMod PmapperModule
pmapperError error
vendors *knownawsaccountslookup.Vendors
// Main module data
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
func (m *GraphCommand) RunGraphCommand() {
// These struct values are used by the output module
m.output.Verbosity = m.Verbosity
m.output.Directory = m.AWSOutputDirectory
m.output.CallingModule = "graph"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.vendors = knownawsaccountslookup.NewVendorMap()
m.vendors.PopulateKnownAWSAccounts()
m.modLog.Info("Collecting data for graph ingestor...")
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
////////////////
// Accounts
////////////////
accounts := m.collectAccountDataForGraph()
// write data to jsonl file for ingestor to read
fileName := fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "accounts")
// create file and directory if it doesn't exist
if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
m.modLog.Error(err)
return
}
outputFile, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
m.modLog.Error(err)
return
}
defer outputFile.Close()
for _, account := range accounts {
if err := internal.WriteJsonlFile(outputFile, account); err != nil {
m.modLog.Error(err)
return
}
}
////////////////
// Users
////////////////
// users := m.collectUserDataForGraph()
// // write data to jsonl file for ingestor to read
// fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "users")
// // create file and directory if it doesn't exist
// if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
// m.modLog.Error(err)
// return
// }
// outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
// if err != nil {
// m.modLog.Error(err)
// return
// }
// defer outputFile.Close()
// for _, user := range users {
// if err := internal.WriteJsonlFile(outputFile, user); err != nil {
// m.modLog.Error(err)
// return
// }
// }
////////////////
// Roles
////////////////
roles := m.collectRoleDataForGraph()
// write data to jsonl file for ingestor to read
fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "roles")
// create file and directory if it doesn't exist
if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
m.modLog.Error(err)
return
}
outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
m.modLog.Error(err)
return
}
defer outputFile.Close()
for _, role := range roles {
if err := internal.WriteJsonlFile(outputFile, role); err != nil {
m.modLog.Error(err)
return
}
}
ingestor, err := ingestor.NewCloudFoxIngestor()
if err != nil {
return
}
// Pmapper hack
goCtx := context.Background()
sharedLogger.Infof("Verifying connectivity to Neo4J at %s", ingestor.Uri)
if err := ingestor.Driver.VerifyConnectivity(goCtx); err != nil {
sharedLogger.Error(err)
}
defer ingestor.Driver.Close(goCtx)
m.pmapperMod.GenerateCypherStatements(goCtx, ingestor.Driver)
// back to regular stuff
ingestor.Run(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)))
}
func (m *GraphCommand) collectAccountDataForGraph() []models.Account {
//OrganizationsCommandClient := InitOrgClient(m.AWSConfig)
var accounts []models.Account
OrganizationsCommandClient := InitOrgClient(m.AWSConfig)
DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(OrganizationsCommandClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Fatal(err)
}
if DescribeOrgOutput.MasterAccountId == nil {
m.modLog.Error("Organization is not configured")
}
// If the account is not the org mgmt account, it can only see some info about itself and some info about the org mgmt account.
// populate both of them here.
if aws.ToString(DescribeOrgOutput.MasterAccountId) != aws.ToString(m.Caller.Account) {
//create new object of type models.Account for this account
thisAccount := models.Account{
Id: aws.ToString(m.Caller.Account),
Arn: fmt.Sprintf("arn:aws:iam::%s:root", aws.ToString(m.Caller.Account)),
//Name: aws.ToString(m.Caller.Account),
IsOrgMgmt: false,
IsChildAccount: true,
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
}
accounts = append(accounts, thisAccount)
//create new object of type models.Account for the mgmt account
mgmtAccount := models.Account{
Id: aws.ToString(DescribeOrgOutput.MasterAccountId),
Arn: aws.ToString(DescribeOrgOutput.MasterAccountArn),
Email: aws.ToString(DescribeOrgOutput.MasterAccountEmail),
//Status: string(account.Status),
//JoinedMethod: string(account.JoinedMethod),
//JoinedTimestamp: account.JoinedTimestamp.String(),
IsOrgMgmt: true,
IsChildAccount: false,
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
}
accounts = append(accounts, mgmtAccount)
return accounts
} else {
// In this case we are the org mgmt account, so we can see all the accounts in the org.
ListAccounts, err := sdk.CachedOrganizationsListAccounts(OrganizationsCommandClient, aws.ToString(DescribeOrgOutput.MasterAccountId))
if err != nil {
m.modLog.Fatal(err)
}
for _, account := range ListAccounts {
var isMgmtAccount bool
var isChildAccount bool
m.modLog.Info("Account: ", aws.ToString(account.Name))
if aws.ToString(DescribeOrgOutput.MasterAccountId) == aws.ToString(account.Id) {
// this is the org mgmt account
isMgmtAccount = true
isChildAccount = false
} else if DescribeOrgOutput.MasterAccountId == nil {
// this is a standalone account
isChildAccount = false
isMgmtAccount = false
} else {
// this is a child account
isChildAccount = true
isMgmtAccount = false
}
//create new object of type models.Account
account := models.Account{
Id: aws.ToString(account.Id),
Arn: aws.ToString(account.Arn),
Name: aws.ToString(account.Name),
Email: aws.ToString(account.Email),
Status: string(account.Status),
JoinedMethod: string(account.JoinedMethod),
JoinedTimestamp: account.JoinedTimestamp.String(),
IsOrgMgmt: isMgmtAccount,
IsChildAccount: isChildAccount,
OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
OrganizationID: aws.ToString(DescribeOrgOutput.Id),
}
accounts = append(accounts, account)
}
return accounts
}
}
func (m *GraphCommand) collectRoleDataForGraph() []models.Role {
var isAdmin, canPrivEscToAdmin string
// iamClient := InitIAMClient(m.AWSConfig)
// iamSimClient := InitIamCommandClient(iamClient, m.Caller, m.AWSProfile, m.Goroutines)
// localAdminMap := make(map[string]bool)
var roles []models.Role
IAMCommandClient := InitIAMClient(m.AWSConfig)
ListRolesOutput, err := sdk.CachedIamListRoles(IAMCommandClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err)
}
for _, role := range ListRolesOutput {
accountId := strings.Split(aws.ToString(role.Arn), ":")[4]
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
break
}
// if m.pmapperError == nil {
// isAdmin, canPrivEscToAdmin = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, role.Arn)
// } else {
// isAdmin, canPrivEscToAdmin = GetIamSimResult(m.SkipAdminCheck, role.Arn, iamSimClient, localAdminMap)
// }
// for _, row := range m.PermissionRowsFromAllProfiles {
// if row.Arn == aws.ToString(role.Arn) {
// // look for cases where there is a permission that allows sts:assumeRole or *
// // lowercase the action and compare it against the list of checks below
// if strings.EqualFold(row.Action, "sts:AssumeRole") ||
// strings.EqualFold(row.Action, "*") ||
// strings.EqualFold(row.Action, "sts:Assume*") ||
// strings.EqualFold(row.Action, "sts:*") {
// if row.Effect == "Allow" {
// //PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
// if row.Resource == "*" {
// PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
// } else if strings.EqualFold(row.Resource, aws.ToString(role.Arn)) {
// PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
// }
// }
// }
// if row.Effect == "Deny" {
// // Remove the string sts:AssumeRole from the PrivEscPermissions slice
// for i, v := range PrivEscPermissions {
// if v == "sts:AssumeRole" {
// PrivEscPermissions = append(PrivEscPermissions[:i], PrivEscPermissions[i+1:]...)
// }
// }
// }
// }
// }
var TrustedPrincipals []models.TrustedPrincipal
var TrustedServices []models.TrustedService
var TrustedFederatedProviders []models.TrustedFederatedProvider
//var TrustedFederatedSubjects string
var trustedProvider string
var trustedSubjects string
var vendorName string
for _, statement := range trustsdoc.Statement {
for _, principal := range statement.Principal.AWS {
if strings.Contains(principal, ":root") {
//check to see if the accountID is known
accountID := strings.Split(principal, ":")[4]
vendorName = m.vendors.GetVendorNameFromAccountID(accountID)
}
TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
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
@@ -0,0 +1 @@
package collectors
+283
View File
@@ -0,0 +1,283 @@
package ingestor
import (
"bufio"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
log "github.com/sirupsen/logrus"
)
const (
// Neo4j
MergeNodeQueryTemplate = `CALL apoc.merge.node([$labels[0]], {Id: $Id}, $properties, $properties) YIELD node as obj
CALL apoc.create.setLabels(obj, $labels) YIELD node as labeledObj
RETURN labeledObj`
MergeRelationQueryTemplate = `UNWIND $batch as row
CALL apoc.merge.node([row.sourceLabel], apoc.map.fromValues([row.sourceProperty, row.sourceNodeId])) YIELD node as from
CALL apoc.merge.node([row.targetLabel], apoc.map.fromValues([row.targetProperty, row.targetNodeId])) YIELD node as to
CALL apoc.merge.relationship(from, row.relationshipType, {}, row.properties, to) YIELD rel
RETURN rel`
// Using sprintf to insert the label name since the driver doesn't support parameters for labels here
// %[1]s is a nice way to say "insert the first parameter here"
CreateConstraintQueryTemplate = "CREATE CONSTRAINT IF NOT EXISTS FOR (n: %s) REQUIRE n.Id IS UNIQUE"
CreateIndexQueryTemplate = "CREATE INDEX %[1]s_Id IF NOT EXISTS FOR (n: %[1]s) ON (n.Id)"
PostProcessMergeQueryTemplate = `MATCH (n)
WITH n.Id AS Id, COLLECT(n) AS nodesToMerge
WHERE size(nodesToMerge) > 1
CALL apoc.refactor.mergeNodes(nodesToMerge, {properties: 'combine', mergeRels:true})
YIELD node
RETURN count(*);`
)
type Neo4jConfig struct {
Uri string
Username string
Password string
}
type CloudFoxIngestor struct {
Neo4jConfig
//ResultsFile string
Driver neo4j.DriverWithContext
TmpDir string
}
func NewCloudFoxIngestor() (*CloudFoxIngestor, error) {
config := Neo4jConfig{
Uri: "neo4j://localhost:7687",
Username: "neo4j",
Password: "cloudfox",
}
driver, err := neo4j.NewDriverWithContext(config.Uri, neo4j.BasicAuth(config.Username, config.Password, ""))
if err != nil {
return nil, err
}
return &CloudFoxIngestor{
Neo4jConfig: config,
//ResultsFile: resultsFile,
Driver: driver,
}, nil
}
// func unzipToTemp(zipFilePath string) (string, error) {
// // Create a temporary directory to extract the zip file to
// tempDir, err := os.MkdirTemp("", "cloudfox-graph")
// if err != nil {
// return "", err
// }
// // Open the zip file and extract to a temporary directory
// zipfile, err := zip.OpenReader(zipFilePath)
// if err != nil {
// return "", err
// }
// defer zipfile.Close()
// for _, file := range zipfile.File {
// path := filepath.Join(tempDir, file.Name)
// log.Debugf("Extracting file: %s", path)
// fileData, err := file.Open()
// if err != nil {
// return "", err
// }
// defer fileData.Close()
// newFile, err := os.Create(path)
// if err != nil {
// return "", err
// }
// defer newFile.Close()
// if _, err := io.Copy(newFile, fileData); err != nil {
// return "", err
// }
// }
// return tempDir, nil
// }
func (i *CloudFoxIngestor) ProcessFile(path string, info os.FileInfo) error {
log.Infof("Processing file: %s", info.Name())
switch info.Name() {
case "accounts.jsonl":
return i.ProcessFileObjects(path, schema.Account, schema.Account)
case "roles.jsonl":
return i.ProcessFileObjects(path, schema.Role, schema.Role)
// case "servicePrincipals.jsonl":
// return i.ProcessFileObjects(path, schema.GraphServicePrincipal, schema.GraphObject)
// case "applications.jsonl":
// return i.ProcessFileObjects(path, schema.GraphApplication, schema.GraphObject)
// case "devices.jsonl":
// return i.ProcessFileObjects(path, schema.GraphDevice, schema.GraphObject)
// case "directoryRoles.jsonl":
// return i.ProcessFileObjects(path, schema.GraphRole, schema.GraphObject)
// case "subscriptions.jsonl":
// return i.ProcessFileObjects(path, schema.Subscription, schema.ArmResource)
// case "tenants.jsonl":
// return i.ProcessFileObjects(path, schema.Tenant, schema.ArmResource)
// case "rbac.jsonl":
// return i.ProcessFileObjects(path, schema.AzureRbac, "")
default:
return nil
}
}
func (i *CloudFoxIngestor) ProcessFileObjects(path string, objectType schema.NodeLabel, generalType schema.NodeLabel) error {
var object = models.NodeLabelToNodeMap[objectType]
// Open the file
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
// Read the file line by line
scanner := bufio.NewScanner(file)
//Iterate over the lines and create the nodes
for scanner.Scan() {
line := strings.TrimSuffix(scanner.Text(), "\n")
// Skip empty lines
if len(line) > 0 {
if err := json.Unmarshal([]byte(line), &object); err != nil {
log.Errorf("%s : %s", err, line)
continue
}
}
relationships := object.MakeRelationships()
if err := i.InsertDBObjects(object, relationships, []schema.NodeLabel{generalType, objectType}); err != nil {
log.Error(err)
continue
}
}
return nil
}
func (i *CloudFoxIngestor) InsertDBObjects(object schema.Node, relationships []schema.Relationship, labels []schema.NodeLabel) error {
goCtx := context.Background()
var err error
// Insert the node
if object != nil {
nodeMap, err := schema.ConvertCustomTypesToNeo4j(&object)
if err != nil {
log.Errorf("Error converting custom types to neo4j: %s -- %v", err, object)
return err
}
//nodeMap := schema.AsNeo4j(&object)
nodeQueryParams := map[string]interface{}{
"Id": nodeMap["Id"],
"labels": labels,
"properties": nodeMap,
}
_, err = neo4j.ExecuteQuery(goCtx, i.Driver, MergeNodeQueryTemplate, nodeQueryParams, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
log.Errorf("Error inserting node: %s -- %v", err, nodeQueryParams)
return err
}
}
// Insert the relationships
if len(relationships) > 0 {
var relationshipInterface []map[string]interface{}
// Check the default SourceProperty and TargetProperty values
for _, relationship := range relationships {
var currentRelationship map[string]interface{}
if relationship.SourceProperty == "" {
relationship.SourceProperty = "Id"
}
if relationship.TargetProperty == "" {
relationship.TargetProperty = "Id"
}
relationshipBytes, err := json.Marshal(relationship)
if err != nil {
return err
}
if err := json.Unmarshal(relationshipBytes, &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
}
@@ -0,0 +1,44 @@
package models
import "github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
type Account struct {
Id string
Arn string
Email string
Name string
Status string
JoinedMethod string
JoinedTimestamp string
IsOrgMgmt bool
IsChildAccount bool
OrgMgmtAccountID string
OrganizationID string
}
func (a *Account) MakeRelationships() []schema.Relationship {
var relationships []schema.Relationship
// make relationship from children accounts to parent org
if a.IsChildAccount {
// make relationship from child to org mgmt account
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.OrganizationID,
TargetNodeID: a.Id,
SourceLabel: schema.Organization,
TargetLabel: schema.Account,
RelationshipType: schema.Manages,
})
// make relationship from parent org mgmt account to child account
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: a.OrganizationID,
SourceLabel: schema.Account,
TargetLabel: schema.Organization,
RelationshipType: schema.MemberOf,
})
}
return relationships
}
@@ -0,0 +1,11 @@
package models
import (
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
)
var NodeLabelToNodeMap = map[schema.NodeLabel]schema.Node{
schema.Organization: &Organization{},
schema.Account: &Account{},
schema.Role: &Role{},
}
+20
View File
@@ -0,0 +1,20 @@
package models
import (
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
)
type Organization struct {
Id string
OrgId string
Arn string
MasterAccountArn string
MasterAccountId string
MasterAccountEmail string
ChildAccounts []Account
MgmtAccount Account
}
func (o *Organization) MakeRelationships() []schema.Relationship {
return []schema.Relationship{}
}
@@ -0,0 +1,5 @@
package models
type Resource struct {
ARN string
}
+647
View File
@@ -0,0 +1,647 @@
package models
import (
"fmt"
"strings"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/BishopFox/cloudfox/internal/common"
"github.com/dominikbraun/graph"
)
type Role struct {
Id string
AccountID string
ARN string
Name string
TrustsDoc policy.TrustPolicyDocument
TrustedPrincipals []TrustedPrincipal
TrustedServices []TrustedService
TrustedFederatedProviders []TrustedFederatedProvider
CanPrivEscToAdmin string
IsAdmin string
IdValue string
IsAdminP bool
PathToAdminSameAccount bool
PathToAdminCrossAccount bool
}
type TrustedPrincipal struct {
TrustedPrincipal string
ExternalID string
VendorName string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type TrustedService struct {
TrustedService string
AccountID string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type TrustedFederatedProvider struct {
TrustedFederatedProvider string
ProviderShortName string
TrustedSubjects string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
func (a *Role) MakeRelationships() []schema.Relationship {
var relationships []schema.Relationship
//instance := singleton.GetInstance()
// get thisAccount id from role arn
var thisAccount string
if len(a.ARN) >= 25 {
thisAccount = a.ARN[13:25]
} else {
fmt.Sprintf("Could not get account number from this role arn%s", a.ARN)
}
// make a relationship between each role and the account it belongs to
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: thisAccount,
SourceLabel: schema.Role,
TargetLabel: schema.Account,
RelationshipType: schema.MemberOf,
})
for _, TrustedPrincipal := range a.TrustedPrincipals {
//get account id from the trusted principal arn
var trustedPrincipalAccount string
if len(TrustedPrincipal.TrustedPrincipal) >= 25 {
trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25]
} else {
fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal)
}
var PermissionsRowAccount string
// make a TRUSTED_BY relationship between the role and the trusted principal. This does not mean the principal can assume this role, we need more logic to determine that (see below)
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: TrustedPrincipal.TrustedPrincipal,
// TargetNodeID: a.Id,
// SourceLabel: schema.Principal,
// TargetLabel: schema.Role,
// RelationshipType: schema.IsTrustedBy,
// })
// // make a TRUSTS relationship between the trusted principal and this role. This does not mean the principal can assume this role, we need more logic to determine that (see below)
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: a.Id,
// TargetNodeID: TrustedPrincipal.TrustedPrincipal,
// SourceLabel: schema.Role,
// TargetLabel: schema.Principal,
// RelationshipType: schema.Trusts,
// })
// // make a MEMBER_OF relationship between the role and the account
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: TrustedPrincipal.TrustedPrincipal,
// TargetNodeID: trustedPrincipalAccount,
// SourceLabel: schema.Principal,
// TargetLabel: schema.Account,
// RelationshipType: schema.MemberOf,
// })
// if the role trusts a principal in this same account explicitly, then the principal can assume the role
if thisAccount == trustedPrincipalAccount {
// make a CAN_ASSUME relationship between the trusted principal and this role
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
TargetNodeID: a.Id,
SourceLabel: schema.Principal,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
// make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: a.Id,
// TargetNodeID: TrustedPrincipal.TrustedPrincipal,
// SourceLabel: schema.Role,
// TargetLabel: schema.Principal,
// RelationshipType: schema.CanBeAssumedBy,
// })
}
// If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role
// if the role we are looking at trusts root in it's own account
if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) {
// iterate over all rows in AllPermissionsRows
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
// but we only care about the rows that have arns that are in this account
if len(PermissionsRow.Arn) >= 25 {
PermissionsRowAccount = PermissionsRow.Arn[13:25]
} else {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == thisAccount {
// lets only look for rows that have sts:AssumeRole permissions
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
strings.EqualFold(PermissionsRow.Action, "*") ||
strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
// make a CAN_ASSUME relationship between the trusted principal and this role
//evaluate if the princiapl is a user or a role and set a variable accordingly
//var principalType schema.NodeLabel
if strings.EqualFold(PermissionsRow.Type, "User") {
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.User,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.Role,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
}
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: PermissionsRow.Arn,
// TargetNodeID: a.Id,
// SourceLabel: principalType,
// TargetLabel: schema.Role,
// RelationshipType: schema.CanAssumeTest,
// })
// make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: a.Id,
// TargetNodeID: PermissionsRow.Arn,
// SourceLabel: schema.Role,
// TargetLabel: schema.Principal,
// RelationshipType: schema.CanBeAssumedByTest,
// })
}
}
}
}
}
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) {
// iterate over all rows in AllPermissionsRows
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
// but we only care about the rows that have arns that are in this other account
if len(PermissionsRow.Arn) >= 25 {
PermissionsRowAccount = PermissionsRow.Arn[13:25]
} else {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == trustedPrincipalAccount {
// lets only look for rows that have sts:AssumeRole permissions
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
strings.EqualFold(PermissionsRow.Action, "*") ||
strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
// make a CAN_ASSUME relationship between the trusted principal and this role
if strings.EqualFold(PermissionsRow.Type, "User") {
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.User,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssumeCrossAccount,
})
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.User,
TargetLabel: schema.Role,
RelationshipType: schema.CanAccess,
})
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.Role,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssumeCrossAccount,
})
relationships = append(relationships, schema.Relationship{
SourceNodeID: PermissionsRow.Arn,
TargetNodeID: a.Id,
SourceLabel: schema.Role,
TargetLabel: schema.Role,
RelationshipType: schema.CanAccess,
})
}
// // make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal
// relationships = append(relationships, schema.Relationship{
// SourceNodeID: a.Id,
// TargetNodeID: PermissionsRow.Arn,
// SourceLabel: schema.Role,
// TargetLabel: schema.Principal,
// RelationshipType: schema.CanBeAssumedByTest,
// })
}
}
}
}
}
// If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" {
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
TargetNodeID: a.Id,
SourceLabel: schema.Vendor,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
}
}
for _, TrustedService := range a.TrustedServices {
// make relationship from trusted service to this role of type can assume
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedService.TrustedService + "_" + TrustedService.AccountID,
TargetNodeID: a.Id,
SourceLabel: schema.Service,
TargetLabel: schema.Role,
RelationshipType: schema.IsTrustedBy,
})
// make relationship from this role to trusted service of type can be assumed by
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: TrustedService.TrustedService + "_" + TrustedService.AccountID,
SourceLabel: schema.Role,
TargetLabel: schema.Service,
RelationshipType: schema.Trusts,
})
}
for _, TrustedFederatedProvider := range a.TrustedFederatedProviders {
// make relationship from trusted federated provider to this role of type can assume
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
TargetNodeID: a.Id,
SourceLabel: schema.FederatedIdentity,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
// make relationship from this role to trusted federated provider of type can be assumed by
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
SourceLabel: schema.Role,
TargetLabel: schema.FederatedIdentity,
RelationshipType: schema.CanBeAssumedBy,
})
// make relationship from trusted federated provider to this role of type can assume
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
TargetNodeID: a.Id,
SourceLabel: schema.FederatedIdentity,
TargetLabel: schema.Role,
RelationshipType: schema.IsTrustedBy,
})
// make relationship from this role to trusted federated provider of type can be assumed by
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: TrustedFederatedProvider.TrustedFederatedProvider,
SourceLabel: schema.Role,
TargetLabel: schema.FederatedIdentity,
RelationshipType: schema.Trusts,
})
}
return relationships
}
// func (a *Node) GenerateAttributes() map[string]string {
// attributes := make(map[string]string)
// attributes["Id"] = a.Id
// attributes["Name"] = a.Name
// attributes["Type"] = "Role"
// attributes["AccountID"] = a.AccountID
// attributes["ARN"] = a.ARN
// attributes["CanPrivEscToAdmin"] = a.CanPrivEscToAdmin
// attributes["IsAdmin"] = a.IsAdmin
// attributes["IdValue"] = a.IdValue
// return attributes
// }
// func (a *Role) MergeAttributes(newAttributes map[string]string) {
// if a.Id == "" {
// a.Id = newAttributes["Id"]
// }
// if a.Name == "" {
// a.Name = newAttributes["Name"]
// }
// if a.Type == "" {
// a.Type = newAttributes["Type"]
// }
// if a.AccountID == "" {
// a.AccountID = newAttributes["AccountID"]
// }
// if a.ARN == "" {
// a.ARN = newAttributes["ARN"]
// }
// if a.CanPrivEscToAdmin == "" {
// a.CanPrivEscToAdmin = newAttributes["CanPrivEscToAdmin"]
// }
// if a.IsAdmin == "" {
// a.IsAdmin = newAttributes["IsAdmin"]
// }
// if a.IdValue == "" {
// a.IdValue = newAttributes["IdValue"]
// }
// }
func (a *Role) MakeVertices(GlobalGraph graph.Graph[string, string]) {
// make a vertex for this role as populate all of the data in the Role struct as attributes
err := GlobalGraph.AddVertex(
a.Id,
graph.VertexAttribute("Name", a.Name),
graph.VertexAttribute("Type", "Role"),
graph.VertexAttribute("AccountID", a.AccountID),
graph.VertexAttribute("ARN", a.ARN),
graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin),
graph.VertexAttribute("IsAdmin", a.IsAdmin),
graph.VertexAttribute("IdValue", a.IdValue),
)
if err != nil {
if err == graph.ErrVertexAlreadyExists {
fmt.Println(a.Id + " already exists")
}
}
}
// func MakeAllVertices(GlobalRoles []Role, GlobalPmapperGraph aws.PmapperModule) (GlobalGraph graph.Graph[string, string]) {
// // for all nodes in the GlobalPmapperGraph, check to see if they exist in the GlobalRoles slice. If they do, then update the node with the new data. If they don't, then add the node to the GlobalRoles slice
// for _, node := range GlobalPmapperGraph.Nodes {
// // check to see if the node exists in the GlobalRoles slice
// var nodeExists bool
// for _, role := range GlobalRoles {
// if node.Id == role.Id {
// nodeExists = true
// }
// }
// // // make a vertex for this role as populate all of the data in the Role struct as attributes
// // err := GlobalGraph.AddVertex(
// // a.Id,
// // graph.VertexAttribute("Name", a.Name),
// // graph.VertexAttribute("Type", "Role"),
// // graph.VertexAttribute("AccountID", a.AccountID),
// // graph.VertexAttribute("ARN", a.ARN),
// // graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin),
// // graph.VertexAttribute("IsAdmin", a.IsAdmin),
// // graph.VertexAttribute("IdValue", a.IdValue),
// // )
// // if err != nil {
// // if err == graph.ErrVertexAlreadyExists {
// // fmt.Println(a.Id + " already exists")
// // }
// // }
// }
func (a *Role) MakeEdges(GlobalGraph graph.Graph[string, string]) []schema.Relationship {
var relationships []schema.Relationship
// get thisAccount id from role arn
var thisAccount string
if len(a.ARN) >= 25 {
thisAccount = a.ARN[13:25]
} else {
fmt.Sprintf("Could not get account number from this role arn%s", a.ARN)
}
for _, TrustedPrincipal := range a.TrustedPrincipals {
//get account id from the trusted principal arn
var trustedPrincipalAccount string
if len(TrustedPrincipal.TrustedPrincipal) >= 25 {
trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25]
} else {
fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal)
}
var PermissionsRowAccount string
// if the role trusts a principal in this same account explicitly, then the principal can assume the role
if thisAccount == trustedPrincipalAccount {
// make a CAN_ASSUME relationship between the trusted principal and this role
err := GlobalGraph.AddEdge(
TrustedPrincipal.TrustedPrincipal,
a.Id,
graph.EdgeAttribute("AssumeRole", "Same account explicit trust"),
)
if err != nil {
fmt.Println(err)
fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Id + "Same account explicit trust")
}
}
// If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role
// if the role we are looking at trusts root in it's own account
if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) {
err := GlobalGraph.AddVertex(
a.Id,
graph.VertexAttribute("Name", a.Name),
graph.VertexAttribute("Type", "Account"),
graph.VertexAttribute("AccountID", a.AccountID),
)
if err != nil {
if err == graph.ErrVertexAlreadyExists {
fmt.Println(a.Id + " already exists")
}
}
// iterate over all rows in AllPermissionsRows
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
// but we only care about the rows that have arns that are in this account
if len(PermissionsRow.Arn) >= 25 {
PermissionsRowAccount = PermissionsRow.Arn[13:25]
} else {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == thisAccount {
// lets only look for rows that have sts:AssumeRole permissions
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
strings.EqualFold(PermissionsRow.Action, "*") ||
strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
// make a CAN_ASSUME relationship between the trusted principal and this role
//evaluate if the princiapl is a user or a role and set a variable accordingly
//var principalType schema.NodeLabel
if strings.EqualFold(PermissionsRow.Type, "User") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Id,
graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"),
)
if err != nil {
fmt.Println(err)
fmt.Println(PermissionsRow.Arn + a.Id + "Same account root trust and trusted principal has permission to assume role")
}
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Id,
graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"),
)
if err != nil {
fmt.Println(err)
fmt.Println(PermissionsRow.Arn + a.Id + "Same account root trust and trusted principal has permission to assume role")
}
}
}
}
}
}
}
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" {
err := GlobalGraph.AddVertex(
a.Id,
graph.VertexAttribute("Name", a.Name),
graph.VertexAttribute("Type", "Account"),
graph.VertexAttribute("AccountID", a.AccountID),
graph.VertexAttribute("VendorName", TrustedPrincipal.VendorName),
)
if err != nil {
if err == graph.ErrVertexAlreadyExists {
fmt.Println(a.Id + " already exists")
}
}
// If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node
relationships = append(relationships, schema.Relationship{
SourceNodeID: TrustedPrincipal.TrustedPrincipal,
TargetNodeID: a.Id,
SourceLabel: schema.Vendor,
TargetLabel: schema.Role,
RelationshipType: schema.CanAssume,
})
err = GlobalGraph.AddEdge(
//TrustedPrincipal.TrustedPrincipal,
TrustedPrincipal.VendorName,
a.Id,
graph.EdgeAttribute("VendorAssumeRole", "Cross account root trust and trusted principal is a vendor"),
)
if err != nil {
fmt.Println(err)
fmt.Println(TrustedPrincipal.VendorName + a.Id + "Cross account root trust and trusted principal is a vendor")
}
} else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) {
err := GlobalGraph.AddVertex(
a.Id,
graph.VertexAttribute("Name", a.Name),
graph.VertexAttribute("Type", "Account"),
graph.VertexAttribute("AccountID", a.AccountID),
)
if err != nil {
if err == graph.ErrVertexAlreadyExists {
fmt.Println(a.Id + " already exists")
}
}
// iterate over all rows in AllPermissionsRows
for _, PermissionsRow := range common.PermissionRowsFromAllProfiles {
// but we only care about the rows that have arns that are in this other account
if len(PermissionsRow.Arn) >= 25 {
PermissionsRowAccount = PermissionsRow.Arn[13:25]
} else {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == trustedPrincipalAccount {
// lets only look for rows that have sts:AssumeRole permissions
if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
strings.EqualFold(PermissionsRow.Action, "*") ||
strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) {
// make a CAN_ASSUME relationship between the trusted principal and this role
if strings.EqualFold(PermissionsRow.Type, "User") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Id,
graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"),
)
if err != nil {
fmt.Println(err)
fmt.Println(PermissionsRow.Arn + a.Id + "Cross account root trust and trusted principal has permission to assume role")
}
} else if strings.EqualFold(PermissionsRow.Type, "Role") {
err := GlobalGraph.AddEdge(
PermissionsRow.Arn,
a.Id,
graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"),
)
if err != nil {
fmt.Println(err)
fmt.Println(PermissionsRow.Arn + a.Id + "Cross account root trust and trusted principal has permission to assume role")
}
}
}
}
}
}
}
}
}
// pmapper takes care of this part so commenting out for now - but leaving as a placeholder
// for _, TrustedService := range a.TrustedServices {
// // make relationship from trusted service to this role of type can assume
// // make relationship from this role to trusted service of type can be assumed by
// }
for _, TrustedFederatedProvider := range a.TrustedFederatedProviders {
// make relationship from trusted federated provider to this role of type can assume
GlobalGraph.AddVertex(
TrustedFederatedProvider.TrustedFederatedProvider,
graph.VertexAttribute("Type", "FederatedIdentity"),
)
err := GlobalGraph.AddEdge(
TrustedFederatedProvider.TrustedFederatedProvider,
a.Id,
graph.EdgeAttribute("FederatedAssumeRole", "Trusted federated provider"),
)
if err != nil {
fmt.Println(err)
fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Id + "Trusted federated provider")
}
}
return relationships
}
+71
View File
@@ -0,0 +1,71 @@
package models
import (
"fmt"
"github.com/BishopFox/cloudfox/aws/graph/ingester/schema"
)
type User struct {
Id string
ARN string
Name string
IsAdmin string
CanPrivEscToAdmin string
IdValue string
}
func (a *User) MakeRelationships() []schema.Relationship {
// make a relationship between each role and the account it belongs to
relationships := []schema.Relationship{}
// get thisAccount id from user arn
var thisAccount string
if len(a.ARN) >= 25 {
thisAccount = a.ARN[13:25]
} else {
fmt.Sprintf("Could not get account number from this user arn%s", a.ARN)
}
relationships = append(relationships, schema.Relationship{
SourceNodeID: a.Id,
TargetNodeID: thisAccount,
SourceLabel: schema.User,
TargetLabel: schema.Account,
RelationshipType: schema.MemberOf,
Properties: map[string]interface{}{},
})
return relationships
}
func (a *User) GenerateAttributes() map[string]string {
return map[string]string{
"Id": a.Id,
"ARN": a.ARN,
"Name": a.Name,
"IsAdmin": a.IsAdmin,
"CanPrivEscToAdmin": a.CanPrivEscToAdmin,
"IdValue": a.IdValue,
}
}
func (a *User) MergeAttributes(other map[string]string) {
if other["Id"] != "" {
a.Id = other["Id"]
}
if other["ARN"] != "" {
a.ARN = other["ARN"]
}
if other["Name"] != "" {
a.Name = other["Name"]
}
if other["IsAdmin"] != "" {
a.IsAdmin = other["IsAdmin"]
}
if other["CanPrivEscToAdmin"] != "" {
a.CanPrivEscToAdmin = other["CanPrivEscToAdmin"]
}
if other["IdValue"] != "" {
a.IdValue = other["IdValue"]
}
}
+148
View File
@@ -0,0 +1,148 @@
package schema
import (
"fmt"
"reflect"
"github.com/goccy/go-json"
"golang.org/x/exp/slices"
)
type RelationshipType string
type NodeLabel string
type Node interface {
MakeRelationships() []Relationship
}
type Relationship struct {
SourceNodeID string `json:"sourceNodeId"`
TargetNodeID string `json:"targetNodeId"`
SourceLabel NodeLabel `json:"sourceLabel"`
TargetLabel NodeLabel `json:"targetLabel"`
RelationshipType RelationshipType `json:"relationshipType"`
Properties map[string]interface{} `json:"properties"`
SourceProperty string `json:"sourceProperty"`
TargetProperty string `json:"targetProperty"`
}
const (
// Relationships
AssociatedTo RelationshipType = "AssociatedTo"
AttachedTo RelationshipType = "AttachedTo"
Authenticates RelationshipType = "Authenticates"
ConnectedTo RelationshipType = "ConnectedTo"
Contains RelationshipType = "Contains"
Exposes RelationshipType = "Exposes"
HasAccess RelationshipType = "HasAccess"
HasConfig RelationshipType = "HasConfig"
HasDisk RelationshipType = "HasDisk"
HasInstance RelationshipType = "HasInstance"
HasRbac RelationshipType = "HasRbac"
HasRole RelationshipType = "HasRole"
Manages RelationshipType = "Manages"
MemberOf RelationshipType = "MemberOf"
Owns RelationshipType = "Owns"
Represents RelationshipType = "Represents"
Trusts RelationshipType = "Trusts"
IsTrustedBy RelationshipType = "IsTrustedBy"
CanAssume RelationshipType = "CanAssume"
CanAssumeCrossAccount RelationshipType = "CanAssumeCrossAccount"
CanBeAssumedBy RelationshipType = "CanBeAssumedBy"
CanBeAssumedByTest RelationshipType = "CanBeAssumedByTest"
CanAssumeTest RelationshipType = "CanAssumeTest"
CanAccess RelationshipType = "CAN_ACCESS"
)
const (
// Node labels
Resource NodeLabel = "Resource"
Account NodeLabel = "Account"
Organization NodeLabel = "Org"
Service NodeLabel = "Service"
Principal NodeLabel = "Principal"
Vendor NodeLabel = "Vendor"
Role NodeLabel = "Role"
Group NodeLabel = "Group"
User NodeLabel = "User"
FederatedIdentity NodeLabel = "FederatedIdentity"
)
func AsNeo4j(object *Node) map[string]interface{} {
// We don't want to include these fields in the map
fieldsToExclude := []string{"members", "owners", "appRoles", "registeredUsers"}
objectMap, err := json.Marshal(object)
if err != nil {
return nil
}
var objectMapInterface map[string]interface{}
json.Unmarshal(objectMap, &objectMapInterface)
for _, field := range fieldsToExclude {
delete(objectMapInterface, field)
}
// We need to convert flatten maps to an array
// We'll want to keep order of the keys for things like extensionAttributes
for key, value := range objectMapInterface {
_, isMap := value.(map[string]interface{})
if isMap {
var valueArray []string
var keys []string
for k := range value.(map[string]interface{}) {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
valueString := value.(map[string]interface{})[k].(string)
if valueString != "" {
valueArray = append(valueArray, k, valueString)
}
}
objectMapInterface[key] = valueArray
}
}
return objectMapInterface
}
func ConvertCustomTypesToNeo4j(node interface{}) (map[string]interface{}, error) {
result := make(map[string]interface{})
val := reflect.ValueOf(node)
// Handling pointers to structs or interfaces
for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
val = val.Elem()
}
// Check if the value is valid and if it's a struct
if !val.IsValid() || val.Kind() != reflect.Struct {
return nil, fmt.Errorf("invalid input: not a struct or a pointer to a struct")
}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := val.Type().Field(i) // Get the StructField
// Check if the field is a struct or slice of structs and not one of the basic types
if (fieldType.Type.Kind() == reflect.Struct ||
(fieldType.Type.Kind() == reflect.Slice && fieldType.Type.Elem().Kind() == reflect.Struct)) &&
fieldType.Type != reflect.TypeOf([]string{}) &&
fieldType.Type != reflect.TypeOf([]int{}) &&
fieldType.Type != reflect.TypeOf([]bool{}) {
// Convert complex field to JSON string
jsonStr, err := json.Marshal(field.Interface())
if err != nil {
return nil, err
}
result[fieldType.Name] = string(jsonStr)
} else {
// Directly use the field for primitive types
result[fieldType.Name] = field.Interface()
}
}
return result, nil
}
+73 -31
View File
@@ -27,9 +27,10 @@ type IamSimulatorModule struct {
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
WrapTable bool
Goroutines int
AWSProfileProvided string
AWSProfileStub string
WrapTable bool
// Main module data
SimulatorResults []SimulatorResult
@@ -76,14 +77,19 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
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.AWSProfile, aws.ToString(m.Caller.Account)))
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
var filename string
var actionList []string
var pmapperCommands []string
var pmapperOutFileName string
var inputArn string
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
if m.AWSProfileProvided == "" {
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
} else {
m.AWSProfileStub = m.AWSProfileProvided
}
wg := new(sync.WaitGroup)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
@@ -102,37 +108,69 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
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.AWSProfile), principal, action)
m.output.FullFilename = 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.AWSProfileStub), principal, action)
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
actionList = append(actionList, action)
m.getPolicySimulatorResult((&principal), actionList, resource, dataReceiver)
// 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)
}
} 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.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)
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)
}
}
} 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.AWSProfile), action)
m.output.FullFilename = 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.AWSProfileStub), action)
filename = 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(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))
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))
}
} 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.AWSProfile), aws.ToString(m.Caller.Account))
m.output.FullFilename = 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.AWSProfileStub), aws.ToString(m.Caller.Account))
filename = m.output.CallingModule
m.executeChecks(wg, resource, dataReceiver)
for _, action := range defaultActionNames {
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))
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))
}
}
}
@@ -199,7 +237,7 @@ 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.AWSProfile, aws.ToString(m.Caller.Account)))
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
@@ -212,12 +250,12 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
Header: m.output.Headers,
Body: m.output.Body,
TableCols: tableCols,
Name: m.output.CallingModule,
Name: filename,
})
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.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)
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][%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]\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)
@@ -225,9 +263,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.AWSProfile))
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] 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)
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 *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pmapperCommands []string) {
@@ -240,7 +278,11 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
outFile := filepath.Join(path, "iam-simulator-pmapper-commands.txt")
var out string
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfile)
if m.AWSProfileProvided != "" {
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfileProvided)
} else {
out = out + fmt.Sprintf("pmapper graph create\n")
}
for _, line := range pmapperCommands {
out = out + line
}
@@ -252,12 +294,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.AWSProfile), 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.AWSProfileStub), 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.AWSProfile), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("End of loot file."))
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), outFile)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), outFile)
}
+52 -35
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
@@ -23,12 +24,13 @@ import (
type InstancesModule struct {
// General configuration data
EC2Client sdk.AWSEC2ClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
EC2Client sdk.AWSEC2ClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSOutputType string
AWSTableCols string
PmapperDataBasePath string
Goroutines int
UserDataAttributesOnly bool
@@ -93,8 +95,8 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -490,9 +492,8 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
var profile string
var externalIP string
var name string = ""
var adminRole string = ""
var roleArn string = ""
var profileArn, profileName, name 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 {
@@ -512,34 +513,50 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
if instance.IamInstanceProfile == nil {
profile = "NoInstanceProfile"
} else {
// 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]
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(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: "",
}
}
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: roleArn,
Region: region,
Admin: adminRole,
CanPrivEsc: "",
}
} else {
profileArn = aws.ToString(instance.IamInstanceProfile.Arn)
// 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 {
log.Printf("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: "",
}
}
}
}
func (m *InstancesModule) getRolesFromInstanceProfiles() {
+11 -9
View File
@@ -31,13 +31,15 @@ type LambdasModule struct {
AWSOutputType string
AWSTableCols string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
iamSimClient IamSimulatorModule
// Main module data
Lambdas []Lambda
@@ -75,8 +77,8 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
fmt.Printf("[%s][%s] Enumerating lambdas for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
+132 -1
View File
@@ -50,6 +50,7 @@ type OutboundAssumeRoleEntry struct {
SourcePrincipal string
DestinationAccount string
DestinationPrincipal string
Action string
LogTimestamp string
}
@@ -117,6 +118,24 @@ type CloudTrailEvent struct {
} `json:"tlsDetails"`
}
var interestingCrossAccountEventNames = []string{
"AssumeRole",
"AssumeRoleWithSAML",
"AssumeRoleWithWebIdentity",
"GetObject",
"ListBuckets",
"BatchGetImage",
"GetDownloadUrlForLayer",
"SendMessage",
"GetQueueUrl",
"Invoke20150331",
"RunInstances",
"RunTask",
"StartTask",
"CreateTask",
"CreateTaskSet",
}
func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
@@ -171,6 +190,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
@@ -194,6 +214,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
// Otherwise, use the default columns.
@@ -206,6 +227,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
"Source Principal",
//"Destination Account",
"Destination Principal",
"Action",
"Log Entry Timestamp",
}
}
@@ -222,6 +244,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir
m.OutboundAssumeRoleEntries[i].SourcePrincipal,
//m.OutboundAssumeRoleEntries[i].DestinationAccount,
m.OutboundAssumeRoleEntries[i].DestinationPrincipal,
m.OutboundAssumeRoleEntries[i].Action,
m.OutboundAssumeRoleEntries[i].LogTimestamp,
},
)
@@ -279,9 +302,12 @@ func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup,
m.modLog.Error(err)
}
if res {
// wg.Add(1)
// m.CommandCounter.Total++
// m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
wg.Add(1)
m.CommandCounter.Total++
m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
m.getCrossAccountBatchGetImageEntriesPerRegion(r, wg, semaphore, dataReceiver)
}
}
@@ -359,6 +385,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
SourcePrincipal: sourcePrincipal,
DestinationAccount: destinationAccount,
DestinationPrincipal: destinationPrincipal,
Action: "sts:AssumeRole",
LogTimestamp: logTimestamp,
}
}
@@ -376,3 +403,107 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
}
}
// get cross account batch get image entries
func (m *OutboundAssumedRolesModule) getCrossAccountBatchGetImageEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
//var LookupAttributes []types.LookupAttributes
//var LookupAttribute types.LookupAttribute
var pages int
days := 0 - m.Days
endTime := aws.Time(time.Now())
startTime := endTime.AddDate(0, 0, days)
for {
LookupEvents, err := m.CloudTrailClient.LookupEvents(
context.TODO(),
&cloudtrail.LookupEventsInput{
EndTime: endTime,
StartTime: &startTime,
// LookupAttributes: []cloudtrailTypes.LookupAttribute{
// {
// AttributeKey: cloudtrailTypes.LookupAttributeKeyEventName,
// AttributeValue: aws.String("BatchGetImage"),
// },
// },
NextToken: PaginationControl,
},
func(o *cloudtrail.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, event := range LookupEvents.Events {
//eventData := *event.CloudTrailEvent
//fmt.Println(eventData)
var sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal, userType string
cloudtrailEvent := CloudTrailEvent{}
json.Unmarshal([]byte(*event.CloudTrailEvent), &cloudtrailEvent)
for _, eventName := range interestingCrossAccountEventNames {
if aws.ToString(event.EventName) == eventName {
// extract the source account and principal
if cloudtrailEvent.UserIdentity.Type == "AssumedRole" || cloudtrailEvent.UserIdentity.Type == "IAMUser" || cloudtrailEvent.UserIdentity.Type == "Role" {
if cloudtrailEvent.UserIdentity.Type == "AssumedRole" {
sourcePrincipal = cloudtrailEvent.UserIdentity.SessionContext.SessionIssuer.Arn
} else {
sourcePrincipal = cloudtrailEvent.UserIdentity.Arn
}
userType = cloudtrailEvent.UserIdentity.Type
sourceAccount = cloudtrailEvent.UserIdentity.AccountID
//fmt.Printf("%s,%s,%s,%s\n", sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal)
if cloudtrailEvent.Resources != nil {
destinationAccount = cloudtrailEvent.Resources[0].AccountID
destinationPrincipal = cloudtrailEvent.Resources[0].Arn
if sourceAccount != destinationAccount {
logTimestamp := cloudtrailEvent.EventTime.Format("2006-01-02 15:04:05")
dataReceiver <- OutboundAssumeRoleEntry{
AWSService: "CloudTrail",
Region: r,
Type: userType,
SourceAccount: sourceAccount,
SourcePrincipal: sourcePrincipal,
DestinationAccount: destinationAccount,
DestinationPrincipal: destinationPrincipal,
Action: aws.ToString(event.EventName),
LogTimestamp: logTimestamp,
}
}
}
}
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if LookupEvents.NextToken != nil {
PaginationControl = LookupEvents.NextToken
pages++
} else {
PaginationControl = nil
break
}
}
}
+18 -27
View File
@@ -11,6 +11,7 @@ import (
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/BishopFox/cloudfox/internal/common"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
@@ -36,7 +37,7 @@ type IamPermissionsModule struct {
Users []GAADUser
Roles []GAADRole
Groups []GAADGroup
Rows []PermissionsRow
Rows []common.PermissionsRow
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
@@ -71,26 +72,12 @@ type GAADGroup struct {
InlinePolicies []types.PolicyDetail
}
type PermissionsRow struct {
AWSService string
Type string
Name string
Arn string
PolicyType string
PolicyName string
PolicyArn string
Effect string
Action string
Resource string
Condition string
}
func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbosity int, principal string) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "permissions"
m.output.FullFilename = m.output.CallingModule
filename := m.output.CallingModule
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
@@ -101,11 +88,11 @@ 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 != "" {
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
}
m.getGAAD()
m.parsePermissions(principal)
m.GetGAAD()
m.ParsePermissions(principal)
m.output.Headers = []string{
"Account",
@@ -192,7 +179,7 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
Header: m.output.Headers,
TableCols: tableCols,
Body: m.output.Body,
Name: m.output.CallingModule,
Name: filename,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
@@ -206,10 +193,10 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *IamPermissionsModule) getGAAD() {
func (m *IamPermissionsModule) GetGAAD() {
GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
TxtLogger.Error(err.Error())
m.CommandCounter.Error++
return
}
@@ -297,7 +284,7 @@ func (m *IamPermissionsModule) getPrincipalArn(principal string) string {
return arn
}
func (m *IamPermissionsModule) parsePermissions(principal string) {
func (m *IamPermissionsModule) ParsePermissions(principal string) {
var inputArn string
for i := range m.Roles {
if principal == "" {
@@ -422,6 +409,10 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
//parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
document, _ := url.QueryUnescape(aws.ToString(d.Document))
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
// hasStsAssumeRole := parsedPolicyDocument.DoesPolicyHaveMatchingStatement("Allow", "sts:AssumeRole", "*")
// fmt.Println(hasStsAssumeRole)
for _, s = range parsedPolicyDocument.Statement {
//version := parsedPolicyDocument.Version
effect := s.Effect
@@ -435,7 +426,7 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
}
m.Rows = append(
m.Rows,
PermissionsRow{
common.PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
@@ -462,7 +453,7 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
}
m.Rows = append(
m.Rows,
PermissionsRow{
common.PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
@@ -508,7 +499,7 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline
}
m.Rows = append(
m.Rows,
PermissionsRow{
common.PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
@@ -534,7 +525,7 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline
}
m.Rows = append(
m.Rows,
PermissionsRow{
common.PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
+354 -23
View File
@@ -1,6 +1,7 @@
package aws
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
@@ -10,9 +11,11 @@ import (
"strings"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/dominikbraun/graph"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/sirupsen/logrus"
)
@@ -28,15 +31,22 @@ type PmapperModule struct {
WrapTable bool
// Main module data
pmapperGraph graph.Graph[string, string]
Nodes []Node
Edges []Edge
CommandCounter internal.CommandCounter
PmapperDataBasePath string
pmapperGraph graph.Graph[string, string]
Nodes []Node
Edges []Edge
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type PmapperOutputRow struct {
Start string
End string
Paths []string
}
type Edge struct {
Source string `json:"source"`
Destination string `json:"destination"`
@@ -45,19 +55,54 @@ type Edge struct {
}
type Node struct {
Arn string `json:"arn"`
IDValue string `json:"id_value"`
AttachedPolicies []AttachedPolicies `json:"attached_policies"`
GroupMemberships []interface{} `json:"group_memberships"`
TrustPolicy interface{} `json:"trust_policy"`
InstanceProfile interface{} `json:"instance_profile"`
ActivePassword bool `json:"active_password"`
AccessKeys int `json:"access_keys"`
IsAdmin bool `json:"is_admin"`
PermissionsBoundary interface{} `json:"permissions_boundary"`
HasMfa bool `json:"has_mfa"`
Tags Tags `json:"tags"`
PathToAdmin bool
Arn string `json:"arn"`
Type string
AccountID string
Name string
IDValue string `json:"id_value"`
AttachedPolicies []AttachedPolicies `json:"attached_policies"`
GroupMemberships []interface{} `json:"group_memberships"`
TrustPolicy interface{} `json:"trust_policy"`
TrustsDoc policy.TrustPolicyDocument
TrustedPrincipals []TrustedPrincipal
TrustedServices []TrustedService
TrustedFederatedProviders []TrustedFederatedProvider
InstanceProfile interface{} `json:"instance_profile"`
ActivePassword bool `json:"active_password"`
AccessKeys int `json:"access_keys"`
IsAdmin bool `json:"is_admin"`
PathToAdmin bool
PermissionsBoundary interface{} `json:"permissions_boundary"`
HasMfa bool `json:"has_mfa"`
Tags Tags `json:"tags"`
CanPrivEscToAdminString string
IsAdminString string
VendorName string
}
type TrustedPrincipal struct {
TrustedPrincipal string
ExternalID string
VendorName string
AccountIsInAnalyzedAccountList bool
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type TrustedService struct {
TrustedService string
AccountID string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type TrustedFederatedProvider struct {
TrustedFederatedProvider string
ProviderAccountId string
ProviderShortName string
TrustedSubjects []string
//IsAdmin bool
//CanPrivEscToAdmin bool
}
type AttachedPolicies struct {
@@ -79,9 +124,16 @@ func (m *PmapperModule) initPmapperGraph() error {
for i := range m.Nodes {
if m.doesNodeHavePathToAdmin(m.Nodes[i]) {
m.Nodes[i].PathToAdmin = true
m.Nodes[i].CanPrivEscToAdminString = "Yes"
//fmt.Println(m.Nodes[i].Arn, m.Nodes[i].IsAdmin, m.Nodes[i].PathToAdmin)
} else {
m.Nodes[i].PathToAdmin = false
m.Nodes[i].CanPrivEscToAdminString = "No"
}
if m.Nodes[i].IsAdmin {
m.Nodes[i].IsAdminString = "Yes"
} else {
m.Nodes[i].IsAdminString = "No"
}
}
@@ -107,7 +159,39 @@ func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] {
}
for _, edge := range m.Edges {
_ = pmapperGraph.AddEdge(edge.Source, edge.Destination)
err := pmapperGraph.AddEdge(
edge.Source,
edge.Destination,
graph.EdgeAttribute(edge.ShortReason, edge.Reason),
)
if err != nil {
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
//fmt.Println("Edge already exists, but adding a new one!")
// get the existing edge
existingEdge, _ := pmapperGraph.Edge(edge.Source, edge.Destination)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}
// Add or update the attribute
existingProperties.Attributes[edge.ShortReason] = edge.Reason
//Update the edge
pmapperGraph.UpdateEdge(
edge.Source,
edge.Destination,
graph.EdgeAttributes(existingProperties.Attributes),
)
}
//fmt.Println(edge.Reason)
}
}
//internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration)
@@ -119,7 +203,11 @@ func (m *PmapperModule) DoesPrincipalHavePathToAdmin(principal string) bool {
for i := range m.Nodes {
if m.Nodes[i].Arn == principal {
if m.Nodes[i].PathToAdmin {
m.Nodes[i].CanPrivEscToAdminString = "Yes"
return true
} else {
m.Nodes[i].CanPrivEscToAdminString = "No"
return false
}
}
@@ -131,7 +219,11 @@ func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool {
for i := range m.Nodes {
if m.Nodes[i].Arn == principal {
if m.Nodes[i].IsAdmin {
m.Nodes[i].IsAdminString = "Yes"
return true
} else {
m.Nodes[i].IsAdminString = "No"
return false
}
}
@@ -234,6 +326,9 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int)
Table: internal.TableClient{
Wrap: m.WrapTable,
},
Loot: internal.LootClient{
DirectoryName: m.output.FilePath,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
@@ -243,7 +338,21 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int)
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
header, body := m.createPmapperTableData(outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: header,
Body: body,
Name: "pmapper-privesc-paths-enhanced",
SkipPrintToScreen: true,
})
loot := m.writeLoot(o.Table.DirectoryName, verbosity)
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
Name: m.output.CallingModule,
Contents: loot,
})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s principals who are admin or have a path to admin identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
@@ -264,9 +373,6 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
for _, p := range path {
if p != "" {
if startNode.Arn != destNode.Arn {
// if we got here there is a path
//fmt.Printf("%s has a path %s who is an admin.\n", startNode.Arn, destNode.Arn)
//fmt.Println(path)
return true
}
@@ -278,9 +384,136 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
return false
}
func (m *PmapperModule) createPmapperTableData(outputDirectory string) ([]string, [][]string) {
var header []string
var body [][]string
header = []string{
"Start",
"End",
"Path(s)",
}
var paths string
var admins, privescPathsBody [][]string
for _, startNode := range m.Nodes {
if startNode.IsAdmin {
admins = append(admins, []string{startNode.Arn, "", "ADMIN"})
} else {
for _, destNode := range m.Nodes {
if destNode.IsAdmin {
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
// if we have a path,
if len(path) > 0 {
if startNode.Arn != destNode.Arn {
paths = ""
// if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen
for i := 0; i < len(path)-1; i++ {
for _, edge := range m.Edges {
if edge.Source == path[i] && edge.Destination == path[i+1] {
//Some pmapper reasons have commas in them so lets get rid of them in the csvOutputdata
edge.Reason = strings.ReplaceAll(edge.Reason, ",", " and")
paths += fmt.Sprintf("%s %s %s\n", path[i], edge.Reason, path[i+1])
}
}
}
//trim the last newline from csvPaths
paths = strings.TrimSuffix(paths, "\n")
privescPathsBody = append(privescPathsBody, []string{startNode.Arn, destNode.Arn, paths})
}
}
}
}
}
}
// create body by first adding the admins and then the privesc paths
body = append(body, admins...)
body = append(body, privescPathsBody...)
return header, body
}
func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
panic(err.Error())
}
lootFilePath := filepath.Join(path, "pmapper.txt")
var admins, out string
for _, startNode := range m.Nodes {
if startNode.IsAdmin {
admins += fmt.Sprintf("ADMIN FOUND: %s\n", startNode.Arn)
} else {
for _, destNode := range m.Nodes {
if destNode.IsAdmin {
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
// if we have a path,
if len(path) > 0 {
if startNode.Arn != destNode.Arn {
// if we got here there is a path
out += fmt.Sprintf("PATH TO ADMIN FOUND\n Start: %s\n End: %s\n Path(s):\n", startNode.Arn, destNode.Arn)
//fmt.Println(path)
// if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen
for i := 0; i < len(path)-1; i++ {
for _, edge := range m.Edges {
if edge.Source == path[i] && edge.Destination == path[i+1] {
// print it like this: [start node] [reason] [end node]
out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1])
}
// shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice
//for _, edge := range GlobalPmapperEdges {
// if edge.Source == path[i] && edge.Destination == path[i+1] {
// // print it like this: [start node] [reason] [end node]
// out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1])
// }
// }
}
}
out += fmt.Sprintf("\n")
}
}
}
}
}
}
out = admins + "\n\n" + out
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Beginning of loot file"))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file"))
}
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), magenta(fmt.Sprintf("Loot file with ALL potential paths written to: [%s]", lootFilePath)))
return out
}
func (m *PmapperModule) readPmapperData(accountID *string) error {
e, n := generatePmapperDataBasePaths(accountID)
var e, n string
if m.PmapperDataBasePath == "" {
e, n = generatePmapperDataBasePaths(accountID)
} else {
e = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "edges.json")
n = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "nodes.json")
}
nodesFile, err := os.Open(n)
if err != nil {
@@ -301,3 +534,101 @@ func (m *PmapperModule) readPmapperData(accountID *string) error {
return nil
}
func (m *PmapperModule) GenerateCypherStatements(goCtx context.Context, driver neo4j.DriverWithContext) error {
// Insert nodes
for i, node := range m.Nodes {
query, params := m.generateNodeCreateStatement(node, i)
if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil {
return err
}
}
// Insert edges
for i, edge := range m.Edges {
query, params := m.generateEdgeCreateStatement(edge, i)
if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil {
return err
}
}
return nil
}
func (m *PmapperModule) generateNodeCreateStatement(node Node, i int) (string, map[string]interface{}) {
var ptype, label, query string
var params map[string]any
if strings.Contains(node.Arn, "role") {
label = GetResourceNameFromArn(node.Arn)
ptype = "Role"
params = map[string]any{
"Id": node.Arn,
"ARN": node.Arn,
"Name": GetResourceNameFromArn(node.Arn),
"IdValue": node.IDValue,
"IsAdminP": node.IsAdmin,
"PathToAdmin": node.PathToAdmin,
}
} else if strings.Contains(node.Arn, "user") {
label = GetResourceNameFromArn(node.Arn)
ptype = "User"
//node.TrustPolicy = ""
params = map[string]any{
"Id": node.Arn,
"ARN": node.Arn,
"Name": GetResourceNameFromArn(node.Arn),
"IdValue": node.IDValue,
"IsAdminP": node.IsAdmin,
"PathToAdmin": node.PathToAdmin,
}
} else if strings.Contains(node.Arn, "group") {
label = GetResourceNameFromArn(node.Arn)
ptype = "Group"
}
label = strings.ReplaceAll(label, "-", "_")
label = strings.ReplaceAll(label, ".", "_")
query = `MERGE (%s:%s {Id: $Id, ARN: $ARN, Name: $Name, IdValue: $IdValue, IsAdminP: $IsAdminP, PathToAdmin: $PathToAdmin})`
//sanitizedArn := sanitizeArnForNeo4jLabel(node.Arn)
//id := fmt.Sprintf("%s_%s", sanitizedArn, ptype)
fmt.Println(fmt.Sprintf(query, label, ptype), params)
return fmt.Sprintf(query, label, ptype), params
}
func (m *PmapperModule) generateEdgeCreateStatement(edge Edge, i int) (string, map[string]interface{}) {
// Sanitize ARNs for matching nodes
//srcArnSanitized := sanitizeArnForNeo4jLabel(edge.Source)
//destArnSanitized := sanitizeArnForNeo4jLabel(edge.Destination)
query := `MATCH (a {ARN: $srcArn}), (b {ARN: $destArn}) CREATE (a)-[:CAN_ACCESS {reason: $reason, shortReason: $shortReason}]->(b)`
params := map[string]any{
"srcArn": edge.Source,
"destArn": edge.Destination,
"reason": edge.Reason,
"shortReason": edge.ShortReason,
}
fmt.Println(query, params)
return query, params
}
func (m *PmapperModule) executeCypherQuery(ctx context.Context, driver neo4j.DriverWithContext, query string, params map[string]interface{}) error {
_, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j"))
if err != nil {
sharedLogger.Errorf("Error executing query: %s -- %v", err, params)
return err
}
return nil
}
func sanitizeArnForNeo4jLabel(arn string) string {
// Replace non-allowed characters with underscores or other allowed characters
sanitized := strings.ReplaceAll(arn, ":", "_")
sanitized = strings.ReplaceAll(sanitized, "-", "_")
// Add more replacements if needed
return sanitized
}
+34 -30
View File
@@ -20,16 +20,17 @@ import (
type ResourceTrustsModule struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
WrapTable bool
AWSOutputType string
AWSTableCols string
AWSMFAToken string
AWSProfile string
CloudFoxVersion string
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
WrapTable bool
AWSOutputType string
AWSTableCols string
AWSMFAToken string
AWSConfig aws.Config
AWSProfileProvided string
AWSProfileStub string
CloudFoxVersion string
Resources2 []Resource2
CommandCounter internal.CommandCounter
@@ -62,13 +63,16 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
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.AWSProfile, aws.ToString(m.Caller.Account)))
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
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, Glue, Lambda, SecretsManager, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
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 Resources with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: CodeBuild, ECR, EFS, Glue, Lambda, SecretsManager, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
@@ -172,16 +176,16 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
TableCols: tableCols,
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.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 resource policies found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
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] 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.AWSProfile))
fmt.Printf("[%s][%s] No resource policies found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), 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.AWSProfileStub), m.output.CallingModule)
}
@@ -292,7 +296,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.WrapTable, m.AWSMFAToken)
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.WrapTable, m.AWSMFAToken)
ListTopics, err := cloudFoxSNSClient.listTopics(r)
if err != nil {
@@ -368,7 +372,7 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.AWSMFAToken)
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.AWSMFAToken)
ListBuckets, err := sdk.CachedListBuckets(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account))
if err != nil {
@@ -451,7 +455,7 @@ func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGrou
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
ListQueues, err := cloudFoxSQSClient.listQueues(r)
if err != nil {
@@ -513,7 +517,7 @@ func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGro
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
DescribeRepositories, err := sdk.CachedECRDescribeRepositories(cloudFoxECRClient.ECRClient, aws.ToString(m.Caller.Account), r)
if err != nil {
@@ -577,7 +581,7 @@ func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, w
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
ListProjects, err := sdk.CachedCodeBuildListProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r)
if err != nil {
@@ -651,7 +655,7 @@ func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitG
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
ListFunctions, err := cloudFoxLambdaClient.listFunctions(r)
if err != nil {
@@ -716,7 +720,7 @@ func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *s
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
ListFileSystems, err := sdk.CachedDescribeFileSystems(cloudFoxEFSClient.EFSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
@@ -786,7 +790,7 @@ func (m *ResourceTrustsModule) getSecretsManagerSecretsPoliciesPerRegion(r strin
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxSecretsManagerClient := InitSecretsManagerClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
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 {
@@ -852,7 +856,7 @@ func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sy
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxGlueClient := InitGlueClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
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 {
+243 -136
View File
@@ -1,22 +1,19 @@
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"
)
@@ -34,14 +31,18 @@ type RoleTrustsModule struct {
AWSOutputType string
AWSTableCols string
pmapperMod PmapperModule
pmapperError error
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
iamSimClient IamSimulatorModule
// Main module data
AnalyzedRoles []AnalyzedRole
RoleTrustTable []RoleTrustRow
vendors *knownawsaccountslookup.Vendors
// Used to store output data for pretty printing
output internal.OutputData2
@@ -62,46 +63,12 @@ type RoleTrustRow struct {
type AnalyzedRole struct {
roleARN *string
trustsDoc trustPolicyDocument
trustsDoc policy.TrustPolicyDocument
// trustType string // UNUSED FIELD, PLEASE REVIEW
Admin string
CanPrivEsc string
}
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(outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
@@ -113,10 +80,13 @@ 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.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
// if m.pmapperError != nil {
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// } else {
@@ -134,6 +104,7 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
}
}
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -152,7 +123,14 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
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,
@@ -176,12 +154,12 @@ 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 principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
fmt.Printf("[%s][%s] %s service 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 principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
fmt.Printf("[%s][%s] %s federated 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))
}
@@ -228,6 +206,14 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin
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)
}
}
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
@@ -247,6 +233,7 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin
RoleTrustRow.CanPrivEsc})
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
@@ -254,6 +241,78 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin
}
// 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 == "" {
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: statement.Condition.StringEquals.StsExternalID,
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
}
func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string, [][]string, []string) {
var header []string
var body [][]string
@@ -279,7 +338,7 @@ func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string,
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
tableCols = []string{"Account", "Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
@@ -347,7 +406,7 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
tableCols = strings.Split(m.AWSTableCols, ",")
// If the user specified wide as the output format, use these columns.
} else if m.AWSOutputType == "wide" {
tableCols = []string{"Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
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?"}
@@ -360,23 +419,25 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
for _, role := range m.AnalyzedRoles {
for _, statement := range role.trustsDoc.Statement {
if len(statement.Principal.Federated) > 0 {
provider, subject := parseFederatedTrustPolicy(statement)
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedFederatedProvider: provider,
TrustedFederatedSubject: subject,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
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})
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedFederatedProvider,
RoleTrustRow.TrustedFederatedSubject,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
@@ -387,47 +448,130 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
}
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!!!"
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 {
column3 = trustedRepos
subjects = append(subjects, "ALL REPOS!!!")
}
} 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
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 {
column3 = "ALL SERVICE ACCOUNTS!"
subjects = append(subjects, "ALL SERVICE ACCOUNTS!!!")
}
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
column2 = "Cognito" // (" + statement.Principal.Federated[0] + ")"
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
column3 = statement.Condition.ForAnyValueStringLike.CognitoAMR
// 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")
}
} 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] + ")"
// 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"
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
subjects = append(subjects, statement.Condition.ForAnyValueStringLike.CognitoAMR)
} else {
subjects = append(subjects, "ALL IDENTITIES")
}
// 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")
}
// 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 column2, column3
return provider, subjects
}
func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
@@ -447,7 +591,7 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
}
for _, role := range ListRoles {
trustsdoc, err := parseRoleTrustPolicyDocument(role)
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
@@ -467,40 +611,3 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
}
}
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")
}
+340 -1
View File
@@ -6,16 +6,51 @@ 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"
)
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() {
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{})
}
// create a CachedApiGatewayGetRestAPIs function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination
@@ -51,6 +86,310 @@ 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 {
// Convert cached data back to GetStagesOutput before returning
cachedOutput := cached.(*CachedGetStagesOutput) // Ensure this type assertion matches your caching logic
return fromCachedGetStagesOutput(cachedOutput), nil
}
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 {
return cached.([]apiGatewayTypes.Resource), nil
}
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 {
return cached.([]apiGatewayTypes.DomainName), nil
}
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 {
return cached.([]apiGatewayTypes.BasePathMapping), nil
}
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 {
// Convert cached data back to GetMethodOutput before returning
cachedOutput := cached.(*CachedGetMethodOutput) // Ensure this type assertion matches your caching logic
return fromCachedGetMethodOutput(cachedOutput), nil
}
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 {
return cached.([]apiGatewayTypes.UsagePlan), nil
}
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 {
return cached.([]apiGatewayTypes.UsagePlanKey), nil
}
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
}
+117 -2
View File
@@ -15,12 +15,127 @@ func (m *MockedAWSAPIGatewayClient) GetRestApis(ctx context.Context, input *apig
return &apigateway.GetRestApisOutput{
Items: []apiGatewayTypes.RestApi{
{
Id: aws.String("api1"),
Id: aws.String("abcdefg"),
Name: aws.String("api1"),
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
Types: []apiGatewayTypes.EndpointType{
apiGatewayTypes.EndpointTypePrivate,
},
},
},
{
Id: aws.String("api2"),
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
+163
View File
@@ -14,10 +14,22 @@ import (
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() {
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) {
@@ -55,3 +67,154 @@ 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 {
return cached.([]domainName), nil
}
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 {
return cached.([]apiGatwayV2Types.ApiMapping), nil
}
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 {
return cached.([]apiGatwayV2Types.Stage), nil
}
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 {
return cached.([]apiGatwayV2Types.Route), nil
}
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
}
+60 -4
View File
@@ -15,12 +15,68 @@ func (m *MockedAWSAPIGatewayv2Client) GetApis(ctx context.Context, input *apigat
return &apigatewayv2.GetApisOutput{
Items: []apiGatwayV2Types.Api{
{
ApiId: aws.String("api1"),
Name: aws.String("api1"),
ApiId: aws.String("asdfsdfasdf"),
Name: aws.String("api1"),
ApiEndpoint: aws.String("https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com"),
},
{
ApiId: aws.String("api2"),
Name: aws.String("api2"),
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
+2 -1
View File
@@ -20,6 +20,7 @@ type CodeBuildClientInterface interface {
func init() {
gob.Register(codeBuildTypes.Project{})
gob.Register([]codeBuildTypes.Project{})
}
@@ -87,7 +88,7 @@ func CachedCodeBuildBatchGetProjects(CodeBuildClient CodeBuildClientInterface, a
return project, err
}
internal.Cache.Set(cacheKey, BatchGetProjects.Projects[0], cache.DefaultExpiration)
internal.Cache.Set(cacheKey, BatchGetProjects.Projects, cache.DefaultExpiration)
return BatchGetProjects.Projects[0], nil
}
+1 -1
View File
@@ -21,7 +21,7 @@ type DocDBClientInterface interface {
func init() {
gob.Register([]docdbTypes.GlobalCluster{})
gob.Register([]docdbTypes.DBCluster{})
//gob.Register([]docdbTypes.DBCluster{})
//gob.Register([]docdbTypes.DBInstance{})
}
+95
View File
@@ -0,0 +1,95 @@
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"
)
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 {
return cached.([]dsTypes.DirectoryDescription), nil
}
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 {
return cached.([]dsTypes.Trust), nil
}
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
}
+6 -4
View File
@@ -10,10 +10,6 @@ import (
type MockedAWSDynamoDBClient struct {
}
func (m *MockedAWSDynamoDBClient) ListTables() ([]string, error) {
return []string{"table1", "table2"}, nil
}
func (m *MockedAWSDynamoDBClient) DescribeTable(ctx context.Context, input *dynamodb.DescribeTableInput, options ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
return &dynamodb.DescribeTableOutput{
Table: &dynamodbTypes.TableDescription{
@@ -21,3 +17,9 @@ func (m *MockedAWSDynamoDBClient) DescribeTable(ctx context.Context, input *dyna
},
}, 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
}
+27
View File
@@ -23,6 +23,7 @@ type AWSIAMClientInterface interface {
SimulatePrincipalPolicy(ctx context.Context, params *iam.SimulatePrincipalPolicyInput, optFns ...func(*iam.Options)) (*iam.SimulatePrincipalPolicyOutput, error)
ListInstanceProfiles(ctx context.Context, params *iam.ListInstanceProfilesInput, optFns ...func(*iam.Options)) (*iam.ListInstanceProfilesOutput, error)
ListGroups(ctx context.Context, params *iam.ListGroupsInput, optFns ...func(*iam.Options)) (*iam.ListGroupsOutput, error)
GetInstanceProfile(ctx context.Context, params *iam.GetInstanceProfileInput, optFns ...func(*iam.Options)) (*iam.GetInstanceProfileOutput, error)
}
func init() {
@@ -34,6 +35,7 @@ func init() {
gob.Register([]iamTypes.InstanceProfile{})
gob.Register([]iamTypes.EvaluationResult{})
gob.Register(customGAADOutput{})
gob.Register(iamTypes.InstanceProfile{})
}
func CachedIamListUsers(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.User, error) {
@@ -288,3 +290,28 @@ func CachedIamListGroups(IAMClient AWSIAMClientInterface, accountID string) ([]i
return Groups, nil
}
func CachedIamGetInstanceProfile(IAMClient AWSIAMClientInterface, accountID string, instanceProfileName string) (iamTypes.InstanceProfile, error) {
var InstanceProfile iamTypes.InstanceProfile
cacheKey := fmt.Sprintf("%s-iam-GetInstanceProfile-%s", accountID, instanceProfileName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(iamTypes.InstanceProfile), nil
}
GetInstanceProfile, err := IAMClient.GetInstanceProfile(
context.TODO(),
&iam.GetInstanceProfileInput{
InstanceProfileName: &instanceProfileName,
},
)
if err != nil {
return InstanceProfile, err
}
InstanceProfile = *GetInstanceProfile.InstanceProfile
internal.Cache.Set(cacheKey, InstanceProfile, cache.DefaultExpiration)
return InstanceProfile, nil
}
-1
View File
@@ -21,7 +21,6 @@ func init() {
//need to do this to avoid conflicts with the Instance type in the ec2 package
type lightsailInstance lightsailTypes.Instance
gob.Register([]lightsailInstance{})
gob.Register([]lightsailTypes.ContainerService{})
}
+39
View File
@@ -14,10 +14,13 @@ import (
type RDSClientInterface interface {
DescribeDBInstances(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error)
DescribeDBClusters(context.Context, *rds.DescribeDBClustersInput, ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error)
}
func init() {
gob.Register([]rdsTypes.DBInstance{})
gob.Register([]rdsTypes.DBCluster{})
}
func CachedRDSDescribeDBInstances(client RDSClientInterface, accountID string, region string) ([]rdsTypes.DBInstance, error) {
@@ -55,3 +58,39 @@ func CachedRDSDescribeDBInstances(client RDSClientInterface, accountID string, r
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
return instances, nil
}
func CachedRDSDescribeDBClusters(client RDSClientInterface, accountID string, region string) ([]rdsTypes.DBCluster, error) {
var PaginationControl *string
var clusters []rdsTypes.DBCluster
cacheKey := fmt.Sprintf("%s-rds-DescribeDBClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]rdsTypes.DBCluster), nil
}
for {
DescribeDBClusters, err := client.DescribeDBClusters(
context.TODO(),
&rds.DescribeDBClustersInput{
Marker: PaginationControl,
},
func(o *rds.Options) {
o.Region = region
},
)
if err != nil {
return clusters, err
}
clusters = append(clusters, DescribeDBClusters.DBClusters...)
//pagination
if DescribeDBClusters.Marker == nil {
break
}
PaginationControl = DescribeDBClusters.Marker
}
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
return clusters, nil
}
+52 -2
View File
@@ -9,10 +9,10 @@ import (
rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
)
type MOckedRDSClient struct {
type MockedRDSClient struct {
}
func (m *MOckedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.DescribeDBInstancesInput, options ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) {
func (m *MockedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.DescribeDBInstancesInput, options ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) {
return &rds.DescribeDBInstancesOutput{
DBInstances: []rdsTypes.DBInstance{
{
@@ -32,3 +32,53 @@ func (m *MOckedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.De
},
}, nil
}
func (m *MockedRDSClient) DescribeDBClusters(ctx context.Context, input *rds.DescribeDBClustersInput, options ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error) {
return &rds.DescribeDBClustersOutput{
DBClusters: []rdsTypes.DBCluster{
{
DBClusterIdentifier: aws.String("db1"),
Engine: aws.String("aurora-postgresql"),
EngineVersion: aws.String("13.3"),
Endpoint: aws.String("db1.cluster-123456789012.us-west-2.rds.amazonaws.com"),
ClusterCreateTime: aws.Time(time.Now()),
MasterUsername: aws.String("postgres"),
Port: aws.Int32(5432),
AssociatedRoles: nil,
},
{
DBClusterIdentifier: aws.String("db2"),
Engine: aws.String("aurora-postgresql"),
EngineVersion: aws.String("13.3"),
Endpoint: aws.String("db2.cluster-123456789012.us-west-2.rds.amazonaws.com"),
ClusterCreateTime: aws.Time(time.Now()),
MasterUsername: aws.String("postgres"),
Port: aws.Int32(5432),
AssociatedRoles: nil,
},
{
DBClusterIdentifier: aws.String("db3"),
Engine: aws.String("neptune"),
EngineVersion: aws.String("1.2.1.0"),
Endpoint: aws.String("db3.cluster-123456789012.us-west-2.neptune.amazonaws.com"),
MasterUsername: aws.String("neptune"),
Port: aws.Int32(8182),
AssociatedRoles: []rdsTypes.DBClusterRole{
{
RoleArn: aws.String("arn:aws:iam::123456789012:role/NeptuneRole"),
Status: aws.String("active"),
},
},
},
{
DBClusterIdentifier: aws.String("db4"),
Engine: aws.String("docsdb"),
EngineVersion: aws.String("4.0.0"),
Endpoint: aws.String("db4.cluster-123456789012.us-west-2.docdb.amazonaws.com"),
MasterUsername: aws.String("docsdb"),
Port: aws.Int32(27017),
AssociatedRoles: nil,
},
},
}, nil
}
+4 -4
View File
@@ -66,10 +66,10 @@ func (m *MockedS3Client) GetBucketPolicy(ctx context.Context, input *s3.GetBucke
func (m *MockedS3Client) GetPublicAccessBlock(ctx context.Context, input *s3.GetPublicAccessBlockInput, options ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) {
return &s3.GetPublicAccessBlockOutput{
PublicAccessBlockConfiguration: &s3Types.PublicAccessBlockConfiguration{
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
BlockPublicAcls: aws.Bool(true),
BlockPublicPolicy: aws.Bool(true),
IgnorePublicAcls: aws.Bool(true),
RestrictPublicBuckets: aws.Bool(true),
},
}, nil
}
+5 -4
View File
@@ -77,11 +77,12 @@ func isRoleAdmin(iamSimMod IamSimulatorModule, principal *string) bool {
}
func initPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) (PmapperModule, error) {
func InitPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int, PmapperDataBasePath string) (PmapperModule, error) {
pmapperMod := PmapperModule{
Caller: Caller,
AWSProfile: AWSProfile,
Goroutines: Goroutines,
Caller: Caller,
AWSProfile: AWSProfile,
Goroutines: Goroutines,
PmapperDataBasePath: PmapperDataBasePath,
}
err := pmapperMod.initPmapperGraph()
if err != nil {
+556
View File
@@ -0,0 +1,556 @@
package aws
import (
"context"
"fmt"
"log"
"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"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type WorkloadsModule struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSProfile string
AWSOutputType string
AWSTableCols string
Goroutines int
WrapTable bool
SkipAdminCheck bool
// Service Clients
EC2Client sdk.AWSEC2ClientInterface
ECSClient sdk.AWSECSClientInterface
LambdaClient sdk.LambdaClientInterface
AppRunnerClient sdk.AppRunnerClientInterface
IAMClient sdk.AWSIAMClientInterface
//LightsailClient sdk.MockedLightsailClient
//SagemakerClient *sagemaker.Client
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string
iamSimClient IamSimulatorModule
InstanceProfileToRolesMap map[string][]iamTypes.Role
// Main module data
Workloads []Workload
CommandCounter internal.CommandCounter
modLog *logrus.Entry
// Used to store output data for pretty printing
output internal.OutputData2
}
type Workload struct {
AWSService string
Region string
Type string
Name string
Arn string
Role string
Admin string
CanPrivEsc string
Public string
}
func (m *WorkloadsModule) PrintWorkloads(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "workloads"
localAdminMap := make(map[string]bool)
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 compute workloads in all regions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: App Runner, EC2, ECS, Lambda \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)
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 Workload)
// Create a channel to signal to stop
receiverDone := make(chan bool)
// Create a channel to signal to stop
go m.Receiver(dataReceiver, receiverDone)
// Get roles from instance profiles to use for EC2 instance to role lookups
m.getRolesFromInstanceProfiles()
for _, region := range m.AWSRegions {
wg.Add(1)
m.CommandCounter.Pending++
go m.executeChecks(region, wg, semaphore, dataReceiver)
}
wg.Wait()
// Perform role analysis
if m.pmapperError == nil {
for i := range m.Workloads {
m.Workloads[i].Admin, m.Workloads[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Workloads[i].Role)
}
} else {
for i := range m.Workloads {
m.Workloads[i].Admin, m.Workloads[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Workloads[i].Role, m.iamSimClient, localAdminMap)
}
}
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
// Table headers
m.output.Headers = []string{
"Account",
"Service",
"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",
"Service",
"Region",
"Arn",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
// Otherwise, use the default columns.
} else {
tableCols = []string{
"Service",
"Region",
"Name",
"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?")
}
sort.Slice(m.Workloads, func(i, j int) bool {
return m.Workloads[i].AWSService < m.Workloads[j].AWSService
})
// Table rows
for i := range m.Workloads {
// If the role is an admin or can privesc to admin, make it magenta
if m.Workloads[i].Admin == "YES" || m.Workloads[i].CanPrivEsc == "YES" {
m.Workloads[i].AWSService = magenta(m.Workloads[i].AWSService)
m.Workloads[i].Region = magenta(m.Workloads[i].Region)
m.Workloads[i].Name = magenta(m.Workloads[i].Name)
m.Workloads[i].Role = magenta(m.Workloads[i].Role)
m.Workloads[i].Admin = magenta(m.Workloads[i].Admin)
m.Workloads[i].CanPrivEsc = magenta(m.Workloads[i].CanPrivEsc)
}
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Workloads[i].AWSService,
m.Workloads[i].Region,
m.Workloads[i].Name,
m.Workloads[i].Arn,
m.Workloads[i].Role,
m.Workloads[i].Admin,
m.Workloads[i].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)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
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 contains only the admin roles or the ones that can privesc to admin. Call another function to do this.
body := m.adminOnlyResults(m.output.Headers, m.output.Body)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: body,
TableCols: tableCols,
Name: fmt.Sprintf("%s-admin", m.output.CallingModule),
SkipPrintToScreen: true,
})
o.WriteFullOutput(o.Table.TableFiles, nil)
fmt.Printf("[%s][%s] %s compute workloads found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No compute workloads 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 *WorkloadsModule) Receiver(receiver chan Workload, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Workloads = append(m.Workloads, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *WorkloadsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, _ := servicemap.IsServiceInRegion("ec2", r)
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getEC2WorkloadsPerRegion(r, wg, semaphore, dataReceiver)
}
res, _ = servicemap.IsServiceInRegion("ecs", r)
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getECSWorkloadsPerRegion(r, wg, semaphore, dataReceiver)
}
res, err := servicemap.IsServiceInRegion("lambda", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getLambdaWorkloadsPerRegion(r, wg, semaphore, dataReceiver)
}
// AppRunner is not supported in the aws service region catalog so we have to run it in all regions
m.CommandCounter.Total++
wg.Add(1)
go m.getAppRunnerWorkloadsPerRegion(r, wg, semaphore, dataReceiver)
}
func (m *WorkloadsModule) getEC2WorkloadsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// Get EC2 instances
ec2Instances, err := sdk.CachedEC2DescribeInstances(m.EC2Client, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err)
}
for _, instance := range ec2Instances {
var profileArn, profileName, name 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 {
if *tag.Key == "Name" {
name = *tag.Value
}
}
if name == "" {
name = aws.ToString(instance.InstanceId)
}
if instance.IamInstanceProfile != nil {
profileArn = aws.ToString(instance.IamInstanceProfile.Arn)
// 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 {
log.Printf("failed to get instance profile for %s, %v", profileArn, err)
continue
}
for _, role := range profileOutput.Roles {
dataReceiver <- Workload{
AWSService: "EC2",
Region: r,
Type: "instance",
Name: name,
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", r, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)),
Role: aws.ToString(role.Arn),
}
}
}
}
}
func (m *WorkloadsModule) getECSWorkloadsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// Get ECS clusters
ecsClusters, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err)
}
for _, cluster := range ecsClusters {
// Get ECS tasks
taskARNs, err := sdk.CachedECSListTasks(m.ECSClient, aws.ToString(m.Caller.Account), r, cluster)
if err != nil {
m.modLog.Error(err)
}
Tasks, err := sdk.CachedECSDescribeTasks(m.ECSClient, aws.ToString(m.Caller.Account), r, cluster, taskARNs)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, task := range Tasks {
var role string
// Get ECS task definition
ecsTaskDefinition, err := sdk.CachedECSDescribeTaskDefinition(m.ECSClient, aws.ToString(m.Caller.Account), r, aws.ToString(task.TaskDefinitionArn))
if err != nil {
m.modLog.Error(err)
}
if ecsTaskDefinition.TaskRoleArn != nil {
role = aws.ToString(ecsTaskDefinition.TaskRoleArn)
}
dataReceiver <- Workload{
AWSService: "ECS",
Region: r,
Type: "task",
Name: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
Arn: aws.ToString(task.TaskArn),
Role: role,
}
}
}
}
func (m *WorkloadsModule) getLambdaWorkloadsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// Get Lambda functions
lambdaFunctions, err := sdk.CachedLambdaListFunctions(m.LambdaClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err)
}
for _, function := range lambdaFunctions {
//var role string
// if function.Role != nil {
// role = aws.ToString(function.Role)
// }
dataReceiver <- Workload{
AWSService: "Lambda",
Region: r,
Type: "function",
Name: aws.ToString(function.FunctionName),
Arn: aws.ToString(function.FunctionArn),
Role: aws.ToString(function.Role),
}
}
}
func (m *WorkloadsModule) getAppRunnerWorkloadsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Workload) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// Get App Runner services
appRunnerServices, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err)
}
for _, service := range appRunnerServices {
var role string
if service.ServiceArn != nil {
role = aws.ToString(service.ServiceArn)
}
dataReceiver <- Workload{
AWSService: "App Runner",
Region: r,
Type: "service",
Name: aws.ToString(service.ServiceName),
Arn: aws.ToString(service.ServiceArn),
Role: role,
}
}
}
func (m *WorkloadsModule) adminOnlyResults(headers []string, body [][]string) [][]string {
var adminOnlyBody [][]string
var adminOnlyHeaders []string
var adminOnlyTableCols []string
var adminColIndex int
var canPrivEscColIndex int
// First find which col is the admin col and which is the can privesc col and simply note which index they are in. Then we can use those indexes to see if the value in the body is "YES".
// For any row where isAdmin or CanPrivEsc is "YES", we will append that row to the adminOnlyBody.
// Find the index of the admin col
for i, header := range headers {
if header == "IsAdminRole?" {
adminOnlyHeaders = append(adminOnlyHeaders, header)
adminOnlyTableCols = append(adminOnlyTableCols, header)
adminColIndex = i
}
}
// Find the index of the can privesc col
for i, header := range headers {
if header == "CanPrivEscToAdmin?" {
adminOnlyHeaders = append(adminOnlyHeaders, header)
adminOnlyTableCols = append(adminOnlyTableCols, header)
canPrivEscColIndex = i
}
}
// Now that we have the indexes of the admin and can privesc cols, we can iterate through the body and append the rows where isAdmin or CanPrivEsc is "YES" to the adminOnlyBody
for _, row := range body {
if row[adminColIndex] == "YES" || row[canPrivEscColIndex] == "YES" {
adminOnlyBody = append(adminOnlyBody, row)
}
}
return adminOnlyBody
}
func (m *WorkloadsModule) getRolesFromInstanceProfiles() {
// The "PaginationControl" value is nil when there's no more data to return.
var PaginationMarker *string
PaginationControl := true
m.InstanceProfileToRolesMap = map[string][]iamTypes.Role{}
for PaginationControl {
ListInstanceProfiles, err := m.IAMClient.ListInstanceProfiles(
context.TODO(),
&(iam.ListInstanceProfilesInput{
Marker: PaginationMarker,
}),
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, instanceProfile := range ListInstanceProfiles.InstanceProfiles {
m.InstanceProfileToRolesMap[aws.ToString(instanceProfile.InstanceProfileId)] = instanceProfile.Roles
}
if aws.ToString(ListInstanceProfiles.Marker) != "" {
PaginationMarker = ListInstanceProfiles.Marker
} else {
PaginationMarker = nil
break
}
}
}
+754 -172
View File
File diff suppressed because it is too large. Load diff
+109
View File
@@ -0,0 +1,109 @@
package cli
import (
"context"
"fmt"
"github.com/BishopFox/cloudfox/gcp/commands"
oauthservice "github.com/BishopFox/cloudfox/gcp/services/oauthService"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var (
// GCP resources filtering options
GCPOrganization string
GCPProjectID string
GCPProjectIDsFilePath string
GCPProjectIDs []string
// Output formatting options
GCPOutputFormat string
GCPOutputDirectory string
GCPVerbosity int
GCPWrapTable bool
// misc options
// GCPIgnoreCache bool
// logger
GCPLogger = internal.NewLogger()
// GCP root command
GCPCommands = &cobra.Command{
Use: "gcp",
Aliases: []string{"gcloud"},
Long: `See "Available Commands" for GCP Modules below`,
Short: "See \"Available Commands\" for GCP Modules below",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if GCPProjectID != "" {
GCPProjectIDs = append(GCPProjectIDs, GCPProjectID)
} else if GCPProjectIDsFilePath != "" {
GCPProjectIDs = internal.LoadFileLinesIntoArray(GCPProjectIDsFilePath)
} else {
GCPLogger.InfoM("project or project-list flags not given, commands requiring a project ID will fail", "gcp")
}
// Create a context with this value to share it with subcommands at runtime
ctx := context.WithValue(context.Background(), "projectIDs", GCPProjectIDs)
// Set the context for this command which all subcommands can access via [SUBCMD].Parent().Context()
// cmd.SetContext(ctx)
os := oauthservice.NewOAuthService()
principal, err := os.WhoAmI()
if err != nil {
GCPLogger.FatalM(fmt.Sprintf("could not determine default user credential with error %s. \n\nPlease use default application default credentials: https://cloud.google.com/docs/authentication/application-default-credentials", err.Error()), "gcp")
}
ctx = context.WithValue(ctx, "account", principal.Email)
cmd.SetContext(ctx)
},
}
)
// New RunAllGCPCommands function to execute all child commands
var GCPAllChecksCommand = &cobra.Command{
Use: "all-checks",
Short: "Runs all available GCP commands",
Long: `Executes all available GCP commands to collect and display information from all supported GCP services.`,
Run: func(cmd *cobra.Command, args []string) {
for _, childCmd := range GCPCommands.Commands() {
if childCmd == cmd { // Skip the run-all command itself to avoid infinite recursion
continue
}
GCPLogger.InfoM(fmt.Sprintf("Running command: %s", childCmd.Use), "all-checks")
childCmd.Run(cmd, args)
}
},
}
func init() {
// Globals flags for the GCP commands
// Allow selection of non-default account to be used when accessing gcloud API
// TODO
// Resource filtering options
// GCPCommands.PersistentFlags().StringVarP(&GCPOrganization, "organization", "o", "", "Organization name or number, repetable")
GCPCommands.PersistentFlags().StringVarP(&GCPProjectID, "project", "p", "", "GCP project ID")
GCPCommands.PersistentFlags().StringVarP(&GCPProjectIDsFilePath, "project-list", "l", "", "Path to a file containing a list of project IDs separated by newlines")
// GCPCommands.PersistentFlags().BoolVarP(&GCPAllProjects, "all-projects", "a", false, "Use all project IDs available to activated gloud account or given gcloud account")
// GCPCommands.PersistentFlags().BoolVarP(&GCPConfirm, "yes", "y", false, "Non-interactive mode (like apt/yum)")
// GCPCommands.PersistentFlags().StringVarP(&GCPOutputFormat, "output", "", "brief", "[\"brief\" | \"wide\" ]")
GCPCommands.PersistentFlags().IntVarP(&Verbosity, "verbosity", "v", 2, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n")
// defaultOutputDir is defined in cli.aws
GCPCommands.PersistentFlags().StringVar(&GCPOutputDirectory, "outdir", defaultOutputDir, "Output Directory ")
// GCPCommands.PersistentFlags().IntVarP(&Goroutines, "max-goroutines", "g", 30, "Maximum number of concurrent goroutines")
GCPCommands.PersistentFlags().BoolVarP(&GCPWrapTable, "wrap", "w", false, "Wrap table to fit in terminal (complicates grepping)")
// Available commands
GCPCommands.AddCommand(
commands.GCPBucketsCommand,
commands.GCPArtifactRegistryCommand,
commands.GCPBigQueryCommand,
commands.GCPSecretsCommand,
commands.GCPIAMCommand,
commands.GCPInstancesCommand,
commands.GCPWhoAmICommand,
GCPAllChecksCommand,
)
}
+172
View File
@@ -0,0 +1,172 @@
package commands
import (
"fmt"
artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
ArtifactRegistryService "github.com/BishopFox/cloudfox/gcp/services/artifactRegistryService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPArtifactRegistryCommand = &cobra.Command{
Use: globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP artifact registry information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available artifact registry resource information:
cloudfox gcp artfact-registry`,
Run: runGCPArtifactRegistryCommand,
}
// Code needed to output fields from buckets results using generic HandleOutput function
// Results struct that implements the internal.OutputInterface
type GCPArtifactRegistryResults struct {
ArtifactData []ArtifactRegistryService.ArtifactInfo
RepositoryData []ArtifactRegistryService.RepositoryInfo
}
// Decide what format the name, header and body of the CSV & JSON files will be
func (g GCPArtifactRegistryResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
repoHeader := []string{
"Name",
"Format",
"Description",
"Size",
"Location",
"ProjectID",
}
var repoBody [][]string
for _, value := range g.RepositoryData {
repoBody = append(
repoBody,
[]string{
value.Name,
value.Format,
value.Description,
value.SizeBytes,
value.Location,
value.ProjectID,
},
)
}
repoTableFile := internal.TableFile{
Header: repoHeader,
Body: repoBody,
Name: fmt.Sprintf("%s-repos", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
}
tableFiles = append(tableFiles, repoTableFile)
artifactHeader := []string{
"Name",
"Format",
"Version",
"Location",
"Repository",
"Size",
"Updated",
"ProjectID",
}
var artifactBody [][]string
for _, value := range g.ArtifactData {
artifactBody = append(
artifactBody,
[]string{
value.Name,
value.Format,
value.Version,
value.Location,
value.Repository,
value.SizeBytes,
value.Updated,
value.ProjectID,
},
)
}
artifactTableFile := internal.TableFile{
Header: artifactHeader,
Body: artifactBody,
Name: fmt.Sprintf("%s-artifacts", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
}
tableFiles = append(tableFiles, artifactTableFile)
return tableFiles
}
// Decide what is loot based on resource information
func (g GCPArtifactRegistryResults) LootFiles() []internal.LootFile {
// TODO consider a loot file of the URLs to the all docker image artifacts. Maybe sample commands to pull the images
return []internal.LootFile{}
}
// Houses high-level logic that retrieves resources and writes to output
func runGCPArtifactRegistryCommand(cmd *cobra.Command, args []string) {
// Retrieve projectIDs from parent (gcp command) ctx
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
client, err := artifactregistry.NewClient(ctx)
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to create secret manager client: %v", err), globals.GCP_SECRETS_MODULE_NAME)
return
}
defer client.Close()
// Get the artifact repositories and artifacts using the projectIDs and ArtifactRegistryService
ars := ArtifactRegistryService.New(client)
var artifactResults []ArtifactRegistryService.ArtifactInfo
var repoRestuls []ArtifactRegistryService.RepositoryInfo
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all artifact repositories and supported artifacts in all locations from project: %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
result, err := ars.RepositoriesAndArtifacts(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return
}
artifactResults = append(artifactResults, result.Artifacts...)
repoRestuls = append(repoRestuls, result.Repositories...)
logger.InfoM(fmt.Sprintf("Done retrieving artifact repository resource data from project: %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
cloudfoxOutput := GCPArtifactRegistryResults{ArtifactData: artifactResults, RepositoryData: repoRestuls}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
}
+134
View File
@@ -0,0 +1,134 @@
package commands
import (
"fmt"
"time"
BigQueryService "github.com/BishopFox/cloudfox/gcp/services/bigqueryService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPBigQueryCommand = &cobra.Command{
Use: "bigquery",
Aliases: []string{},
Short: "Display Bigquery datasets and tables information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available Bigquery datasets and tables resource information:
cloudfox gcp bigquery`,
Run: runGCPBigQueryCommand,
}
// GCPBigQueryResults struct that implements the internal.OutputInterface
type GCPBigQueryResults struct {
DatasetsData []BigQueryService.BigqueryDataset
TablesData []BigQueryService.BigqueryTable
}
// Define the format for CSV & JSON output
func (g GCPBigQueryResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
// For Datasets
datasetHeader := []string{"Name", "DatasetID", "Description", "CreationTime", "LastModifiedTime", "Location", "ProjectID"}
var datasetBody [][]string
for _, dataset := range g.DatasetsData {
datasetBody = append(datasetBody, []string{
dataset.Name,
dataset.DatasetID,
dataset.Description,
dataset.CreationTime.Format(time.RFC3339),
dataset.LastModifiedTime.Format(time.RFC3339),
dataset.Location,
dataset.ProjectID,
})
}
datasetTableFile := internal.TableFile{
Header: datasetHeader,
Body: datasetBody,
Name: "bigquery-datasets",
}
tableFiles = append(tableFiles, datasetTableFile)
// For Tables
tableHeader := []string{"TableID", "DatasetID", "Description", "CreationTime", "LastModifiedTime", "NumBytes", "Location", "ProjectID"}
var tableBody [][]string
for _, table := range g.TablesData {
tableBody = append(tableBody, []string{
table.TableID,
table.DatasetID,
table.Description,
table.CreationTime.Format(time.RFC3339),
table.LastModifiedTime.Format(time.RFC3339),
fmt.Sprintf("%d", table.NumBytes),
table.Location,
table.ProjectID,
})
}
tableTableFile := internal.TableFile{
Header: tableHeader,
Body: tableBody,
Name: "bigquery-tables",
}
tableFiles = append(tableFiles, tableTableFile)
return tableFiles
}
func (g GCPBigQueryResults) LootFiles() []internal.LootFile {
// Implement if there's specific data considered as loot
return []internal.LootFile{}
}
func runGCPBigQueryCommand(cmd *cobra.Command, args []string) {
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_BIGQUERY_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_BIGQUERY_MODULE_NAME)
}
bqService := BigQueryService.New()
var datasetsResults []BigQueryService.BigqueryDataset
var tablesResults []BigQueryService.BigqueryTable
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving BigQuery datasets and tables from project: %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
result, err := bqService.BigqueryDatasetsAndTables(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BIGQUERY_MODULE_NAME)
return
}
datasetsResults = append(datasetsResults, result.Datasets...)
tablesResults = append(tablesResults, result.Tables...)
cloudfoxOutput := GCPBigQueryResults{DatasetsData: datasetsResults, TablesData: tablesResults}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_BIGQUERY_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BIGQUERY_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
}
}
+115
View File
@@ -0,0 +1,115 @@
package commands
import (
"fmt"
CloudStorageService "github.com/BishopFox/cloudfox/gcp/services/cloudStorageService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPBucketsCommand = &cobra.Command{
Use: globals.GCP_BUCKETS_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP buckets information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available bucket information:
cloudfox gcp buckets`,
Run: runGCPBucketsCommand,
}
// Code needed to output fields from buckets results using generic HandleOutput function
// Results struct that implements the internal.OutputInterface
type GCPBucketsResults struct {
Data []CloudStorageService.BucketInfo
}
// Decide what format the name, header and body of the CSV & JSON files will be
func (g GCPBucketsResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
header := []string{
"Name",
"Location",
"ProjectID",
}
var body [][]string
for _, value := range g.Data {
body = append(
body,
[]string{
value.Name,
value.Location,
value.ProjectID,
},
)
}
tableFile := internal.TableFile{
Header: header,
Body: body,
Name: globals.GCP_BUCKETS_MODULE_NAME,
}
tableFiles = append(tableFiles, tableFile)
return tableFiles
}
// Decide what is loot based on resource information
func (g GCPBucketsResults) LootFiles() []internal.LootFile {
return []internal.LootFile{}
}
// Houses high-level logic that retrieves resources and writes to output
func runGCPBucketsCommand(cmd *cobra.Command, args []string) {
// Retrieve projectIDs from parent (gcp command) ctx
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_BUCKETS_MODULE_NAME)
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_BUCKETS_MODULE_NAME)
}
// Get the bucket info using the projectIDs and CloudStorageService
cs := CloudStorageService.New()
var results []CloudStorageService.BucketInfo
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all buckets from project: %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
result, err := cs.Buckets(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BUCKETS_MODULE_NAME)
return
}
results = append(results, result...)
logger.InfoM(fmt.Sprintf("Done retrieving all buckets from project: %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
cloudfoxOutput := GCPBucketsResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_BUCKETS_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BUCKETS_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
}
}
+122
View File
@@ -0,0 +1,122 @@
package commands
import (
"fmt"
IAMService "github.com/BishopFox/cloudfox/gcp/services/iamService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPIAMCommand = &cobra.Command{
Use: globals.GCP_IAM_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP IAM information",
Args: cobra.MinimumNArgs(0),
Long: `
Display IAM principals and their roles information within GCP resources:
cloudfox gcp iam`,
Run: runGCPIAMCommand,
}
// Results struct for IAM command that implements the internal.OutputInterface
type GCPIAMResults struct {
Data []IAMService.PrincipalWithRoles
}
// TableFiles formats the data for table output, CSV & JSON files
func (g GCPIAMResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
header := []string{
"Name",
"Principal Type",
"Role",
"PolicyResourceType",
"PolicyResourceID",
}
var body [][]string
for _, principal := range g.Data {
for _, binding := range principal.PolicyBindings {
body = append(body, []string{
principal.Name,
principal.Type,
binding.Role,
principal.ResourceType,
principal.ResourceID,
})
}
}
tableFile := internal.TableFile{
Header: header,
Body: body,
Name: globals.GCP_IAM_MODULE_NAME,
}
tableFiles = append(tableFiles, tableFile)
return tableFiles
}
// LootFiles can be implemented if needed
func (g GCPIAMResults) LootFiles() []internal.LootFile {
return []internal.LootFile{}
}
// Houses high-level logic that retrieves IAM information and writes to output
func runGCPIAMCommand(cmd *cobra.Command, args []string) {
// Retrieve projectIDs and resource type from parent (gcp command) ctx
var projectIDs []string
var resourceType string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_IAM_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_IAM_MODULE_NAME)
}
// TODO fix once folders or organizations are supported as input for project root
resourceType = "project"
// Initialize IAMService and fetch principals with roles for the given projectIDs and resource type
iamService := IAMService.New()
var results []IAMService.PrincipalWithRoles
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving IAM information for resource: %s of type %s", projectID, resourceType), globals.GCP_IAM_MODULE_NAME)
principals, err := iamService.PrincipalsWithRoles(projectID, resourceType)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_IAM_MODULE_NAME)
return
}
results = append(results, principals...)
logger.InfoM(fmt.Sprintf("Done retrieving IAM information for resource: %s of type %s", projectID, resourceType), globals.GCP_IAM_MODULE_NAME)
cloudfoxOutput := GCPIAMResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_IAM_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_IAM_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_IAM_MODULE_NAME)
}
}
+126
View File
@@ -0,0 +1,126 @@
package commands
import (
"fmt"
ComputeEngineService "github.com/BishopFox/cloudfox/gcp/services/computeEngineService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPInstancesCommand = &cobra.Command{
Use: globals.GCP_INSTANCES_MODULE_NAME, // This should be defined in the globals package
Aliases: []string{},
Short: "Display GCP Compute Engine instances information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available Compute Engine instances information:
cloudfox gcp instances`,
Run: runGCPInstancesCommand,
}
// GCPInstancesResults implements internal.OutputInterface for Compute Engine instances
type GCPInstancesResults struct {
Data []ComputeEngineService.ComputeEngineInfo
}
func (g GCPInstancesResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
header := []string{
"Name",
"ID",
"State",
"ExternalIP",
"InternalIP",
"ServiceAccount", // Adding ServiceAccount to the header
"Zone",
"ProjectID",
}
var body [][]string
for _, instance := range g.Data {
// Initialize an empty string to aggregate service account emails
var serviceAccountEmails string
for _, serviceAccount := range instance.ServiceAccounts {
// Assuming each instance can have multiple service accounts, concatenate their emails
if serviceAccountEmails != "" {
serviceAccountEmails += "; " // Use semicolon as a delimiter for multiple emails
}
serviceAccountEmails += serviceAccount.Email
}
body = append(body, []string{
instance.Name,
instance.ID,
instance.State,
instance.ExternalIP,
instance.InternalIP,
serviceAccountEmails, // Add the aggregated service account emails to the output
instance.Zone,
instance.ProjectID,
})
}
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_INSTANCES_MODULE_NAME,
Header: header,
Body: body,
})
return tableFiles
}
func (g GCPInstancesResults) LootFiles() []internal.LootFile {
// Define any loot files if applicable
return []internal.LootFile{}
}
func runGCPInstancesCommand(cmd *cobra.Command, args []string) {
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_INSTANCES_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_IAM_MODULE_NAME)
}
ces := ComputeEngineService.New()
var results []ComputeEngineService.ComputeEngineInfo
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all instances from project: %s", projectID), globals.GCP_INSTANCES_MODULE_NAME)
result, err := ces.Instances(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_INSTANCES_MODULE_NAME)
return
}
results = append(results, result...)
logger.InfoM(fmt.Sprintf("Done retrieving all instances from project: %s", projectID), globals.GCP_INSTANCES_MODULE_NAME)
cloudfoxOutput := GCPInstancesResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_INSTANCES_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_INSTANCES_MODULE_NAME)
return
}
logger.InfoM("Done writing output", globals.GCP_INSTANCES_MODULE_NAME)
}
}
+120
View File
@@ -0,0 +1,120 @@
package commands
import (
"fmt"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
SecretsService "github.com/BishopFox/cloudfox/gcp/services/secretsService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPSecretsCommand = &cobra.Command{
Use: globals.GCP_SECRETS_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP secrets information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available secrets information:
cloudfox gcp secrets`,
Run: runGCPSecretsCommand,
}
// GCPSecretsResults struct that implements the internal.OutputInterface
type GCPSecretsResults struct {
Data []SecretsService.SecretInfo
}
func (g GCPSecretsResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
header := []string{
"Name",
"CreationTime",
"Labels",
"Rotation",
"ProjectID",
// Add more fields as necessary
}
var body [][]string
for _, value := range g.Data {
body = append(body, []string{
value.Name,
value.CreationTime,
fmt.Sprintf("%v", value.Labels),
value.Rotation,
value.ProjectID,
})
}
tableFile := internal.TableFile{
Header: header,
Body: body,
Name: globals.GCP_SECRETS_MODULE_NAME,
}
tableFiles = append(tableFiles, tableFile)
return tableFiles
}
func (g GCPSecretsResults) LootFiles() []internal.LootFile {
// Define any specific data considered as loot
return []internal.LootFile{}
}
func runGCPSecretsCommand(cmd *cobra.Command, args []string) {
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_SECRETS_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_IAM_MODULE_NAME)
}
client, err := secretmanager.NewClient(ctx)
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to create secret manager client: %v", err), globals.GCP_SECRETS_MODULE_NAME)
return
}
defer client.Close()
ss := SecretsService.New(client)
var results []SecretsService.SecretInfo
// Set output params from parentCmd
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all secrets from project: %s", projectID), globals.GCP_SECRETS_MODULE_NAME)
result, err := ss.Secrets(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_SECRETS_MODULE_NAME)
return
}
results = append(results, result...)
logger.InfoM(fmt.Sprintf("Done retrieving all secrets from project: %s", projectID), globals.GCP_SECRETS_MODULE_NAME)
cloudfoxOutput := GCPSecretsResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_SECRETS_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_SECRETS_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_SECRETS_MODULE_NAME)
}
}
+34
View File
@@ -0,0 +1,34 @@
package commands
import (
"fmt"
OAuthService "github.com/BishopFox/cloudfox/gcp/services/oauthService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPWhoAmICommand = &cobra.Command{
Use: globals.GCP_WHOAMI_MODULE_NAME,
Short: "Display the email address of the GCP authenticated user",
Args: cobra.NoArgs,
Run: runGCPWhoAmICommand,
}
func runGCPWhoAmICommand(cmd *cobra.Command, args []string) {
logger := internal.NewLogger()
// Initialize the OAuthService
oauthService := OAuthService.NewOAuthService()
// Call the WhoAmI function
principal, err := oauthService.WhoAmI()
if err != nil {
logger.ErrorM(fmt.Sprintf("Error retrieving token info: %v", err), globals.GCP_WHOAMI_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("authenticated user email: %s", principal.Email), globals.GCP_WHOAMI_MODULE_NAME)
}
@@ -0,0 +1,236 @@
package artifactregistryservice
import (
"context"
"fmt"
"strings"
artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
"github.com/BishopFox/cloudfox/gcp/services/models"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
locationpb "google.golang.org/genproto/googleapis/cloud/location"
)
// New creates a new instance of the ArtifactRegistryService using an artifactregistry.Client
func New(client *artifactregistry.Client) ArtifactRegistryService {
ars := ArtifactRegistryService{
Client: &ArtifactRegistryClientWrapper{
Closer: client.Close,
RepositoryLister: func(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository] {
return client.ListRepositories(ctx, req, opts...)
},
LocationLister: func(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location] {
return client.ListLocations(ctx, req, opts...)
},
RepositoryGetter: func(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error) {
return client.GetRepository(ctx, req, opts...)
},
DockerImageLister: func(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage] {
return client.ListDockerImages(ctx, req, opts...)
},
},
}
return ars
}
var logger internal.Logger
// RepositoriesAndArtifacts retrieves both repositories and their artifacts for a given projectID.
func (ars *ArtifactRegistryService) RepositoriesAndArtifacts(projectID string) (CombinedRepoArtifactInfo, error) {
var combinedInfo CombinedRepoArtifactInfo
// Retrieve repositories.
repos, err := ars.Repositories(projectID)
if err != nil {
return combinedInfo, fmt.Errorf("failed to retrieve repositories: %v", err)
}
combinedInfo.Repositories = repos
// Iterate over repositories to fetch artifacts.
for _, repo := range repos {
// Extract location and repository name from the repository Name.
parts := strings.Split(repo.Name, "/")
if len(parts) < 6 {
logger.InfoM("Unexpected repository name format, skipping artifacts retrieval", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
continue
}
location := parts[3]
repositoryName := parts[5]
// Fetch artifacts for the current repository.
artifacts, err := ars.Artifacts(projectID, location, repositoryName)
if err != nil {
logger.InfoM(fmt.Sprintf("Failed to retrieve artifacts for repository %s: %v", repositoryName, err), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
continue // Optionally continue to the next repository or handle error differently.
}
combinedInfo.Artifacts = append(combinedInfo.Artifacts, artifacts...)
}
return combinedInfo, nil
}
// RepositoriesAndArtifacts returns a list of repositories and artifacts given projectID.
// TODO consider putting location as input, so that we can put Locations iteration before it & potentially filter by location at top level of GCP module
func (ars *ArtifactRegistryService) Repositories(projectID string) ([]RepositoryInfo, error) {
// Get the list of available locations for the given project.
locations, err := ars.projectLocations(projectID)
if err != nil {
return nil, err
}
var repositories []RepositoryInfo
ctx := context.Background()
// Construct the request for listing repositories.
for _, location := range locations {
req := &artifactregistrypb.ListRepositoriesRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location),
}
it := ars.Client.ListRepositories(ctx, req)
for {
repo, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
repositories = append(repositories, RepositoryInfo{
Name: repo.Name,
Format: repo.Format.String(),
Description: repo.Description,
SizeBytes: fmt.Sprintf("%d", repo.SizeBytes),
ProjectID: projectID,
Location: location,
})
}
}
return repositories, nil
}
// Artifacts fetches the artifacts for a given repository, handling different formats.
func (ars *ArtifactRegistryService) Artifacts(projectID string, location string, repositoryName string) ([]ArtifactInfo, error) {
ctx := context.Background()
// client, err := artifactregistry.NewClient(ctx)
// if err != nil {
// return nil, fmt.Errorf("failed to create artifact registry client: %v", err)
// }
// defer client.Close()
repoFullName := fmt.Sprintf("projects/%s/locations/%s/repositories/%s", projectID, location, repositoryName)
// Fetch repository details to determine its format
repo, err := ars.Client.GetRepository(ctx, &artifactregistrypb.GetRepositoryRequest{Name: repoFullName})
if err != nil {
return nil, fmt.Errorf("failed to get repository details: %v", err)
}
// Handle different repository formats
switch repo.Format {
case artifactregistrypb.Repository_DOCKER:
return ars.DockerImages(repoFullName)
// Placeholder for other formats
// case artifactregistrypb.Repository_MAVEN:
// return listMavenArtifacts(ctx, client, repoFullName)
// case artifactregistrypb.Repository_NPM:
// return listNpmPackages(ctx, client, repoFullName)
default:
logger.InfoM("Repository format is unsupported, so no artifacts were retrieved.", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return nil, nil
}
}
// parseDockerImageName parses the Docker image name string and returns the extracted details.
func parseDockerImageName(imageName string) DockerImageDetails {
// Split the image name by '/'
parts := strings.Split(imageName, "/")
// Extract details based on the known structure of the image name.
// Assuming the format is always consistent as described.
projectID := parts[1]
location := parts[3]
repository := parts[5]
// The image name and digest are after the last '/', separated by '@'
imageAndDigest := strings.Split(parts[7], "@")
imageName = imageAndDigest[0]
digest := imageAndDigest[1]
return DockerImageDetails{
ProjectID: projectID,
Location: location,
Repository: repository,
ImageName: imageName,
Digest: digest,
}
}
// listDockerImages specifically lists Docker images within a given repository.
func (ars *ArtifactRegistryService) DockerImages(repositoryName string) ([]ArtifactInfo, error) {
ctx := context.Background()
var artifacts []ArtifactInfo
req := &artifactregistrypb.ListDockerImagesRequest{
Parent: repositoryName,
}
it := ars.Client.ListDockerImages(ctx, req)
for {
image, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
// Parse image name to extract detailed information.
details := parseDockerImageName(image.Name)
// Populate the ArtifactInfo structure with Docker image details.
artifacts = append(artifacts, ArtifactInfo{
Name: details.ImageName,
Format: "DOCKER",
Location: details.Location,
Repository: details.Repository,
SizeBytes: fmt.Sprintf("%d", image.ImageSizeBytes),
Updated: image.UpdateTime.AsTime().String(),
Digest: details.Digest,
ProjectID: details.ProjectID,
})
}
return artifacts, nil
}
// listProjectLocations returns a list of available locations for the given project ID.
func (ars *ArtifactRegistryService) projectLocations(projectID string) ([]string, error) {
// Define the request to list locations.
req := &locationpb.ListLocationsRequest{
Name: fmt.Sprintf("projects/%s", projectID),
}
// Initialize an empty slice to hold the locations.
var locations []string
// Call the ListLocations method and handle pagination.
ctx := context.Background()
it := ars.Client.ListLocations(ctx, req)
for {
loc, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list locations: %w", err)
}
locations = append(locations, loc.LocationId)
}
return locations, nil
}
@@ -0,0 +1,252 @@
package artifactregistryservice_test
import (
"context"
"fmt"
"reflect"
"testing"
artifactRegistryService "github.com/BishopFox/cloudfox/gcp/services/artifactRegistryService"
"github.com/BishopFox/cloudfox/gcp/services/models"
artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
locationpb "google.golang.org/genproto/googleapis/cloud/location"
)
// Mock structure for ArtifactRegistryClientWrapper
type mockArtifactRegistryClient struct {
repositories []*artifactregistrypb.Repository
locations []*locationpb.Location
dockerImages []*artifactregistrypb.DockerImage
}
func (m *mockArtifactRegistryClient) Close() error {
return nil
}
// Mock iterator for repositories
type mockRepoIterator struct {
repositories []*artifactregistrypb.Repository
index int
}
func (it *mockRepoIterator) Next() (*artifactregistrypb.Repository, error) {
if it.index < len(it.repositories) {
repo := it.repositories[it.index]
it.index++
return repo, nil
}
return nil, iterator.Done
}
// Mock iterator for locations
type mockLocationIterator struct {
locations []*locationpb.Location
index int
}
func (it *mockLocationIterator) Next() (*locationpb.Location, error) {
if it.index < len(it.locations) {
loc := it.locations[it.index]
it.index++
return loc, nil
}
return nil, iterator.Done
}
// Mock iterator for Docker images
type mockDockerImageIterator struct {
dockerImages []*artifactregistrypb.DockerImage
index int
}
func (it *mockDockerImageIterator) Next() (*artifactregistrypb.DockerImage, error) {
if it.index < len(it.dockerImages) {
img := it.dockerImages[it.index]
it.index++
return img, nil
}
return nil, iterator.Done
}
// Mock methods
func (m *mockArtifactRegistryClient) ListRepositories(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository] {
return &mockRepoIterator{repositories: m.repositories}
}
func (m *mockArtifactRegistryClient) ListLocations(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location] {
return &mockLocationIterator{locations: m.locations}
}
func (m *mockArtifactRegistryClient) GetRepository(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error) {
for _, repo := range m.repositories {
if repo.Name == req.Name {
return repo, nil
}
}
return nil, fmt.Errorf("repository not found")
}
func (m *mockArtifactRegistryClient) ListDockerImages(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage] {
return &mockDockerImageIterator{dockerImages: m.dockerImages}
}
var mockClient = mockArtifactRegistryClient{}
var ars = artifactRegistryService.ArtifactRegistryService{
Client: &artifactRegistryService.ArtifactRegistryClientWrapper{
Closer: mockClient.Close,
RepositoryLister: func(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository] {
return mockClient.ListRepositories(ctx, req, opts...)
},
LocationLister: func(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location] {
return mockClient.ListLocations(ctx, req, opts...)
},
RepositoryGetter: func(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error) {
return mockClient.GetRepository(ctx, req, opts...)
},
DockerImageLister: func(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage] {
return mockClient.ListDockerImages(ctx, req, opts...)
},
},
}
// Tests go here
func TestArtifacts(t *testing.T) {
cases := []struct {
name string
projectID string
location string
repositoryName string
setupMockClient func(client *mockArtifactRegistryClient)
expectedArtifacts []artifactRegistryService.ArtifactInfo
expectError bool
}{
{
name: "Valid artifacts retrieval",
projectID: "project1",
location: "us-central1",
repositoryName: "repo1",
setupMockClient: func(client *mockArtifactRegistryClient) {
client.dockerImages = []*artifactregistrypb.DockerImage{
{
Name: "projects/project1/locations/us-central1/repositories/repo1/dockerImages/image1@sha256:e9954c1fc875017be1c3e36eca16be2d9e9bccc4bf072163515467d6a823c7cf",
Uri: "us-central1-docker.pkg.dev/project1/repo1/image1@sha256:e9954c1fc875017be1c3e36eca16be2d9e9bccc4bf072163515467d6a823c7cf",
ImageSizeBytes: 1024,
},
}
client.locations = []*locationpb.Location{
{
LocationId: "us-central1",
},
}
client.repositories = []*artifactregistrypb.Repository{{
Name: "projects/project1/locations/us-central1/repositories/repo1",
Format: artifactregistrypb.Repository_DOCKER,
},
}
},
expectedArtifacts: []artifactRegistryService.ArtifactInfo{
{
Name: "image1",
Format: "DOCKER",
Location: "us-central1",
Repository: "repo1",
SizeBytes: "1024",
ProjectID: "project1",
Digest: "sha256:e9954c1fc875017be1c3e36eca16be2d9e9bccc4bf072163515467d6a823c7cf",
Updated: "1970-01-01 00:00:00 +0000 UTC",
},
},
expectError: false,
},
// Add more test cases here
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Reset mockClient state
mockClient.repositories = nil
mockClient.locations = nil
mockClient.dockerImages = nil
// Setup mock client for the current test case
tc.setupMockClient(&mockClient)
artifacts, err := ars.Artifacts(tc.projectID, tc.location, tc.repositoryName)
if (err != nil) != tc.expectError {
t.Errorf("Artifacts() error = %v, expectError %v", err, tc.expectError)
return
}
if !reflect.DeepEqual(artifacts, tc.expectedArtifacts) {
t.Errorf("Artifacts() = %v, expected %v", artifacts, tc.expectedArtifacts)
}
})
}
}
func TestRepositories(t *testing.T) {
cases := []struct {
name string
projectID string
setupMockClient func(client *mockArtifactRegistryClient)
expectedRepositories []artifactRegistryService.RepositoryInfo
expectError bool
}{
{
name: "Valid repositories retrieval",
projectID: "project1",
setupMockClient: func(client *mockArtifactRegistryClient) {
client.repositories = []*artifactregistrypb.Repository{
{
Name: "projects/project1/locations/us-central1/repositories/repo1",
Format: artifactregistrypb.Repository_DOCKER,
Description: "Test repository",
},
}
client.locations = []*locationpb.Location{
{
LocationId: "us-central1",
},
}
},
expectedRepositories: []artifactRegistryService.RepositoryInfo{
{
Name: "projects/project1/locations/us-central1/repositories/repo1",
Format: "DOCKER",
Description: "Test repository",
SizeBytes: "0",
ProjectID: "project1",
Location: "us-central1",
},
},
expectError: false,
},
// Add more test cases here
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Reset mockClient state
mockClient.repositories = nil
mockClient.locations = nil
mockClient.dockerImages = nil
// Setup mock client for the current test case
tc.setupMockClient(&mockClient)
repositories, err := ars.Repositories(tc.projectID)
if (err != nil) != tc.expectError {
t.Errorf("Repositories() error = %v, expectError %v", err, tc.expectError)
return
}
if !reflect.DeepEqual(repositories, tc.expectedRepositories) {
t.Errorf("Repositories() = %v, expected %v", repositories, tc.expectedRepositories)
}
})
}
}
@@ -0,0 +1,78 @@
package artifactregistryservice
import (
"context"
artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
"github.com/BishopFox/cloudfox/gcp/services/models"
"github.com/googleapis/gax-go/v2"
locationpb "google.golang.org/genproto/googleapis/cloud/location"
)
// CombinedRepoArtifactInfo holds the combined information of repositories and their respective artifacts.
type CombinedRepoArtifactInfo struct {
Repositories []RepositoryInfo `json:"repositories"`
Artifacts []ArtifactInfo `json:"artifacts"`
}
// ArtifactInfo represents the basic information of an artifact within a registry.
type ArtifactInfo struct {
Name string `json:"name"`
Format string `json:"format"`
Version string `json:"version"`
Location string `json:"location"`
Repository string `json:"repository"`
SizeBytes string `json:"virtualSize"`
Updated string `json:"updated"`
Digest string `json:"digest"`
ProjectID string `json:"projectID"`
}
// RepositoryInfo holds information about a repository and its artifacts.
type RepositoryInfo struct {
Name string `json:"name"`
Format string `json:"format"`
Description string `json:"description"`
SizeBytes string `json:"sizeBytes"`
ProjectID string `json:"projectID"`
Location string `json:"location"`
}
// DockerImageDetails holds the extracted parts from a Docker image name.
type DockerImageDetails struct {
ProjectID string `json:"projectID"`
Location string `json:"location"`
Repository string `json:"repository"`
ImageName string `json:"name"`
Digest string `json:"digest"`
}
// Function calls to the API are wrapped to facilitate mocking
type ArtifactRegistryClientWrapper struct {
Closer func() error
RepositoryLister func(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository]
LocationLister func(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location]
RepositoryGetter func(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error)
DockerImageLister func(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage]
}
func (w *ArtifactRegistryClientWrapper) ListRepositories(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository] {
return w.RepositoryLister(ctx, req, opts...)
}
func (w *ArtifactRegistryClientWrapper) ListLocations(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location] {
return w.LocationLister(ctx, req, opts...)
}
func (w *ArtifactRegistryClientWrapper) GetRepository(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error) {
return w.RepositoryGetter(ctx, req, opts...)
}
func (w *ArtifactRegistryClientWrapper) ListDockerImages(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage] {
return w.DockerImageLister(ctx, req, opts...)
}
// ArtifactRegistryService provides methods to interact with Artifact Registry resources.
type ArtifactRegistryService struct {
Client *ArtifactRegistryClientWrapper
}
@@ -0,0 +1,150 @@
package bigqueryservice
import (
"context"
"time"
"cloud.google.com/go/bigquery"
"google.golang.org/api/iterator"
)
// BigqueryDataset represents a dataset in BigQuery
type BigqueryDataset struct {
DatasetID string
Location string
CreationTime time.Time
LastModifiedTime time.Time
Description string
Name string
ProjectID string
}
// BigqueryTable represents a table in BigQuery
type BigqueryTable struct {
TableID string
DatasetID string
Location string
CreationTime time.Time
LastModifiedTime time.Time
NumBytes int64
Description string
ProjectID string
}
// CombinedBigqueryData represents both datasets and tables within a project
type CombinedBigqueryData struct {
Datasets []BigqueryDataset
Tables []BigqueryTable
}
type BigQueryService struct {
// Placeholder for any required services or configuration
}
// New creates a new instance of BigQueryService
func New() *BigQueryService {
return &BigQueryService{}
}
// gcloud alpha bq datasets list
// gcloud alpha bq datasets describe terragoat_dev_dataset
// gcloud alpha bq tables list --dataset terragoat_dev_dataset
// gcloud alpha bq tables describe bar --dataset terragoat_dev_dataset
// BigqueryDatasetsAndTables retrieves all datasets and their tables for a given projectID
func (bq *BigQueryService) BigqueryDatasetsAndTables(projectID string) (CombinedBigqueryData, error) {
// Initialize the combined struct to hold datasets and tables
combinedData := CombinedBigqueryData{}
// Retrieve all datasets
datasets, err := bq.BigqueryDatasets(projectID)
if err != nil {
return combinedData, err
}
combinedData.Datasets = datasets
// Iterate over each dataset to retrieve its tables
for _, dataset := range datasets {
tables, err := bq.BigqueryTables(projectID, dataset.DatasetID)
if err != nil {
return combinedData, err
}
combinedData.Tables = append(combinedData.Tables, tables...)
}
return combinedData, nil
}
// BigqueryDatasets retrieves datasets from the given projectID across all locations
func (bq *BigQueryService) BigqueryDatasets(projectID string) ([]BigqueryDataset, error) {
ctx := context.Background()
client, err := bigquery.NewClient(ctx, projectID)
if err != nil {
return nil, err
}
defer client.Close()
var datasets []BigqueryDataset
it := client.Datasets(ctx)
for {
ds, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
meta, err := ds.Metadata(ctx)
if err != nil {
return nil, err
}
datasets = append(datasets, BigqueryDataset{
DatasetID: ds.DatasetID,
Location: meta.Location,
CreationTime: meta.CreationTime,
LastModifiedTime: meta.LastModifiedTime,
Description: meta.Description,
Name: meta.Name,
ProjectID: projectID,
})
}
return datasets, nil
}
// BigqueryTables retrieves tables from the given projectID and dataset across all locations
func (bq *BigQueryService) BigqueryTables(projectID string, datasetID string) ([]BigqueryTable, error) {
ctx := context.Background()
client, err := bigquery.NewClient(ctx, projectID)
if err != nil {
return nil, err
}
defer client.Close()
var tables []BigqueryTable
ds := client.Dataset(datasetID)
it := ds.Tables(ctx)
for {
table, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
meta, err := table.Metadata(ctx)
if err != nil {
return nil, err
}
tables = append(tables, BigqueryTable{
TableID: table.TableID,
DatasetID: datasetID,
Location: meta.Location,
CreationTime: meta.CreationTime,
LastModifiedTime: meta.LastModifiedTime,
NumBytes: meta.NumBytes,
Description: meta.Description,
ProjectID: projectID,
})
}
return tables, nil
}
@@ -0,0 +1 @@
package bigqueryservice_test
@@ -0,0 +1,117 @@
package cloudstorageservice
import (
"context"
"fmt"
"cloud.google.com/go/storage"
"google.golang.org/api/iterator"
)
type CloudStorageService struct {
// DataStoreService datastoreservice.DataStoreService
}
func New() *CloudStorageService {
return &CloudStorageService{}
}
// type ObjectInfo struct {
// ObjectName string `json:"objecttName"`
// ObjectSizeBytes float64 `json:"objectSizeBytes"`
// IsPublic bool `json:"isPublic"`
// }
type BucketInfo struct {
Name string `json:"name"`
Location string `json:"location"`
ProjectID string `json:"projectID"`
}
func (cs *CloudStorageService) Buckets(projectID string) ([]BucketInfo, error) {
ctx := context.Background()
client, err := storage.NewClient(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to create client: %v", err)
}
defer client.Close()
var buckets []BucketInfo
bucketIterator := client.Buckets(ctx, projectID)
for {
battrs, err := bucketIterator.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
bucket := BucketInfo{Name: battrs.Name, Location: battrs.Location, ProjectID: projectID}
buckets = append(buckets, bucket)
}
return buckets, nil
}
// func (cs *CloudStorageService) BucketsWithMetaData(projectID string) (map[string][]BucketInfo, error) {
// buckets, _ := cs.Buckets(projectID)
// bucketInfos := make(map[string][]BucketInfo)
// ctx := context.Background()
// client, err := storage.NewClient(ctx)
// if err != nil {
// return nil, fmt.Errorf("Failed to create client: %v", err)
// }
// for {
// bucketAttrs, err := buckets.Next()
// if err == iterator.Done {
// break
// }
// if err != nil {
// return nil, fmt.Errorf("failed to list buckets: %v", err)
// }
// bucketName := bucketAttrs.Name
// log.Printf("Working on bucket %s", bucketName)
// // List all objects in the bucket and calculate total size
// totalSize := int64(0)
// var objects []ObjectInfo
// it := client.Bucket(bucketName).Objects(ctx, nil)
// for {
// objectAttrs, err := it.Next()
// if err == iterator.Done {
// break
// }
// if err != nil {
// return nil, fmt.Errorf("failed to list objects in bucket %s: %v", bucketName, err)
// }
// // Get size
// objectSize := objectAttrs.Size
// totalSize += objectSize
// // Check if public
// isPublic := false
// for _, rule := range objectAttrs.ACL {
// if rule.Entity == storage.AllUsers {
// isPublic = true
// break
// }
// }
// objects = append(objects, ObjectInfo{ObjectName: objectAttrs.Name, ObjectSizeBytes: float64(objectSize), IsPublic: isPublic})
// if totalSize > 3221225472 { // 3 GiB in bytes
// log.Printf("%s bucket is over 3 GiB. Skipping remaining objects in this bucket...", bucketName)
// break
// }
// }
// bucketSizeMB := float64(totalSize) / 1024 / 1024
// bucketInfos[projectID] = append(bucketInfos[projectID], BucketInfo{BucketName: bucketName, BucketSizeMB: bucketSizeMB, Objects: objects})
// }
// log.Printf("Sorting resulting list of buckets in descending order %s", projectID)
// sort.Slice(bucketInfos[projectID], func(i, j int) bool {
// return bucketInfos[projectID][i].BucketSizeMB > bucketInfos[projectID][j].BucketSizeMB
// })
// return bucketInfos, nil
// }
@@ -0,0 +1 @@
package cloudstorageservice_test
@@ -0,0 +1,99 @@
package computeengineservice
import (
"context"
"fmt"
"strings"
"google.golang.org/api/compute/v1"
)
type ComputeEngineService struct {
// DataStoreService datastoreservice.DataStoreService
}
func New() *ComputeEngineService {
return &ComputeEngineService{}
}
type ComputeEngineInfo struct {
Name string
ID string
Zone string
State string
ExternalIP string
InternalIP string
ServiceAccounts []*compute.ServiceAccount // Assuming role is derived from service accounts
NetworkInterfaces []*compute.NetworkInterface
Tags *compute.Tags
ProjectID string
}
// Retrieves instances from all regions and zones for a project without using concurrency.
func (ces *ComputeEngineService) Instances(projectID string) ([]ComputeEngineInfo, error) {
ctx := context.Background()
computeService, err := compute.NewService(ctx)
if err != nil {
return nil, err
}
regions, err := computeService.Regions.List(projectID).Do()
if err != nil {
return nil, err
}
var instanceInfos []ComputeEngineInfo
for _, region := range regions.Items {
for _, zoneURL := range region.Zones {
zone := getZoneNameFromURL(zoneURL)
instanceList, err := computeService.Instances.List(projectID, zone).Do()
if err != nil {
return nil, fmt.Errorf("error retrieving instances from zone %s: %v", zone, err)
}
for _, instance := range instanceList.Items {
info := ComputeEngineInfo{
Name: instance.Name,
ID: fmt.Sprintf("%v", instance.Id),
Zone: zoneURL,
State: instance.Status,
ExternalIP: getExternalIP(instance),
InternalIP: getInternalIP(instance),
ServiceAccounts: instance.ServiceAccounts,
NetworkInterfaces: instance.NetworkInterfaces,
Tags: instance.Tags,
ProjectID: projectID,
}
instanceInfos = append(instanceInfos, info)
}
}
}
return instanceInfos, nil
}
// Returns the zone from a GCP URL string with the zone in it
func getZoneNameFromURL(zoneURL string) string {
splits := strings.Split(zoneURL, "/")
return splits[len(splits)-1]
}
// getExternalIP extracts the external IP address from an instance if available.
func getExternalIP(instance *compute.Instance) string {
for _, iface := range instance.NetworkInterfaces {
for _, accessConfig := range iface.AccessConfigs {
if accessConfig.NatIP != "" {
return accessConfig.NatIP
}
}
}
return ""
}
// getInternalIP extracts the internal IP address from an instance.
func getInternalIP(instance *compute.Instance) string {
if len(instance.NetworkInterfaces) > 0 {
return instance.NetworkInterfaces[0].NetworkIP
}
return ""
}
// TODO consider just getting the emails of the service account and returning a []string
@@ -0,0 +1 @@
package computeengineservice
+93
View File
@@ -0,0 +1,93 @@
package iamservice
// import (
// "fmt"
// "strings"
// "time"
// "github.com/fatih/color"
// "github.com/BishopFox/cloudfox/internal/gcp"
// "github.com/BishopFox/cloudfox/internal"
// "github.com/BishopFox/cloudfox/globals"
// )
// type AccessTokensModule struct {
// // Filtering data
// Organizations []string
// Folders []string
// Projects []string
// }
// // Be incorportated in iam.go
// func (m *AccessTokensModule) PrintAccessTokens(outputFormat string, outputDirectory string, verbosity int) error {
// GCPLogger.InfoM(fmt.Sprintf("Enumerating GCP local access tokens (%s, %s)...", color.CyanString("default user token"), color.RedString("application-default token")), globals.GCP_ACCESSTOKENS_MODULE_NAME)
// tokens := gcp.ReadRefreshTokens()
// accessTokens := gcp.ReadAccessTokens()
// applicationdefaulthash := gcp.GetDefaultApplicationHash()
// activeAccount := gcp.GetActiveAccount()
// var tableHead = []string{"Account", "Credential Type", "Validity", "Refresh Token", "Refresh Token needs password"}
// var tableBody [][]string
// for _, accessToken := range accessTokens {
// // prepare token's associated ID (email or hash)
// var emailinfo string
// if (accessToken.AccountID == activeAccount) {
// emailinfo = color.BlueString(accessToken.AccountID)
// } else {
// emailinfo = accessToken.AccountID
// }
// // format expiration time
// Exp, _ := time.Parse(time.RFC3339, accessToken.TokenExpiry)
// var timeinfo string
// if Exp.After(time.Now()) {
// timeinfo = Exp.Format(time.RFC1123)
// } else {
// timeinfo = "EXPIRED"
// }
// // display token type, user or application credential
// var tokentype string
// if (strings.Contains(accessToken.AccountID, "@")) {
// tokentype = "User"
// } else {
// tokentype = "Application"
// }
// // is there an associated refres htoken ?
// var refreshtokeninfo string
// refreshtokeninfo = "No"
// for _, refreshtoken := range tokens {
// if (accessToken.AccountID == refreshtoken.Email) {
// refreshtokeninfo = "Yes"
// } else if (accessToken.AccountID == applicationdefaulthash) {
// refreshtokeninfo = "Yes"
// emailinfo = color.RedString(emailinfo)
// }
// }
// // is there a proof of reauthentication to use the refresh
// // token ? If not, password is needed
// var raptinfo string
// if refreshtokeninfo == "No" {
// raptinfo = "-"
// } else if accessToken.RaptToken.Valid {
// raptinfo = "No"
// } else {
// if (tokentype == "Application") {
// raptinfo = "TBD"
// } else {
// raptinfo = "Yes"
// }
// }
// tableBody = append(
// tableBody,
// []string{
// emailinfo,
// tokentype,
// timeinfo,
// refreshtokeninfo,
// raptinfo,
// })
// }
// internal.PrintTableToScreen(tableHead, tableBody, false)
// return nil
// }
+189
View File
@@ -0,0 +1,189 @@
package iamservice
import (
"context"
"fmt"
"strings"
iampb "cloud.google.com/go/iam/apiv1/iampb"
resourcemanager "cloud.google.com/go/resourcemanager/apiv3"
resourcemanagerpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
)
type IAMService struct {
// DataStoreService datastoreservice.DataStoreService
}
func New() *IAMService {
return &IAMService{}
}
// AncestryResource represents a single resource in the project's ancestry.
type AncestryResource struct {
Type string `json:"type"`
Id string `json:"id"`
}
// PolicyBindings represents IAM policy bindings.
type PolicyBinding struct {
Role string `json:"role"`
Members []string `json:"members"`
ResourceID string `json:"resourceID"`
ResourceType string
PolicyName string `json:"policyBindings"`
Condition string
}
type PrincipalWithRoles struct {
Name string
Type string
PolicyBindings []PolicyBinding
ResourceID string
ResourceType string
}
var logger internal.Logger
func projectAncestry(projectID string) ([]AncestryResource, error) {
ctx := context.Background()
projectsClient, err := resourcemanager.NewProjectsClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create projects client: %v", err)
}
defer projectsClient.Close()
foldersClient, err := resourcemanager.NewFoldersClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create folders client: %v", err)
}
defer foldersClient.Close()
resourceID := "projects/" + projectID
var ancestry []AncestryResource
for {
if strings.HasPrefix(resourceID, "organizations/") {
ancestry = append(ancestry, AncestryResource{Type: "organization", Id: strings.TrimPrefix(resourceID, "organizations/")})
break
} else if strings.HasPrefix(resourceID, "folders/") {
resp, err := foldersClient.GetFolder(ctx, &resourcemanagerpb.GetFolderRequest{Name: resourceID})
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to access folder %s, %v", resourceID, err), globals.GCP_IAM_MODULE_NAME)
break // Stop processing further if a folder is inaccessible
}
ancestry = append(ancestry, AncestryResource{Type: "folder", Id: strings.TrimPrefix(resp.Name, "folders/")})
resourceID = resp.Parent
} else if strings.HasPrefix(resourceID, "projects/") {
resp, err := projectsClient.GetProject(ctx, &resourcemanagerpb.GetProjectRequest{Name: resourceID})
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to access project %s, %v", resourceID, err), globals.GCP_IAM_MODULE_NAME)
return nil, fmt.Errorf("failed to get project: %v", err)
}
ancestry = append(ancestry, AncestryResource{Type: "project", Id: strings.TrimPrefix(resp.Name, "projects/")})
resourceID = resp.Parent
} else {
return nil, fmt.Errorf("unknown resource type for: %s", resourceID)
}
}
// Reverse the slice as we've built it from child to ancestor
for i, j := 0, len(ancestry)-1; i < j; i, j = i+1, j-1 {
ancestry[i], ancestry[j] = ancestry[j], ancestry[i]
}
return ancestry, nil
}
// Policies fetches IAM policy for a given resource and all policies in resource ancestry
func (s *IAMService) Policies(resourceID string, resourceType string) ([]PolicyBinding, error) {
ctx := context.Background()
client, err := resourcemanager.NewProjectsClient(ctx)
if err != nil {
return nil, fmt.Errorf("resourcemanager.NewProjectsClient: %v", err)
}
defer client.Close()
var resourceName string
switch resourceType {
case "project":
resourceName = "projects/" + resourceID
case "folder":
resourceName = "folders/" + resourceID
case "organization":
resourceName = "organizations/" + resourceID
default:
return nil, fmt.Errorf("unsupported resource type: %s", resourceType)
}
req := &iampb.GetIamPolicyRequest{
Resource: resourceName,
}
// Fetch the IAM policy for the resource
policy, err := client.GetIamPolicy(ctx, req)
if err != nil {
return nil, fmt.Errorf("client.GetIamPolicy: %v", err)
}
// Assemble the policy bindings
var policyBindings []PolicyBinding
for _, binding := range policy.Bindings {
policyBinding := PolicyBinding{
Role: binding.Role,
Members: binding.Members,
ResourceID: resourceID,
ResourceType: resourceType,
Condition: binding.Condition.String(),
PolicyName: resourceName + "_policyBindings",
}
policyBindings = append(policyBindings, policyBinding)
}
return policyBindings, nil
}
func determinePrincipalType(member string) string {
if strings.HasPrefix(member, "user:") {
return "User"
} else if strings.HasPrefix(member, "serviceAccount:") {
return "ServiceAccount"
} else if strings.HasPrefix(member, "group:") {
return "Group"
} else {
return "Unknown"
}
}
func (s *IAMService) PrincipalsWithRoles(resourceID string, resourceType string) ([]PrincipalWithRoles, error) {
policyBindings, err := s.Policies(resourceID, resourceType)
if err != nil {
return nil, err
}
principalMap := make(map[string]*PrincipalWithRoles)
for _, pb := range policyBindings {
for _, member := range pb.Members {
principalType := determinePrincipalType(member) // Implement this function based on member prefix
if principal, ok := principalMap[member]; ok {
principal.PolicyBindings = append(principal.PolicyBindings, pb)
} else {
principalMap[member] = &PrincipalWithRoles{
Name: member,
Type: principalType,
PolicyBindings: []PolicyBinding{pb},
ResourceID: resourceID,
ResourceType: resourceType,
}
}
}
}
var principals []PrincipalWithRoles
for _, principal := range principalMap {
principals = append(principals, *principal)
}
return principals, nil
}
@@ -0,0 +1 @@
package iamservice_test
+5
View File
@@ -0,0 +1,5 @@
package models
type GenericIterator[T any] interface {
Next() (*T, error)
}
@@ -0,0 +1,260 @@
package networkservice
import (
"context"
"log"
"net"
"strings"
ComputeEngineService "github.com/BishopFox/cloudfox/gcp/services/computeEngineService"
"google.golang.org/api/compute/v1"
)
// VPC / network / subnets
// gcloud compute networks list
// gcloud compute networks get-effective-firewalls terragoat-dev-network
// gcloud compute networks subnets list
// FirewallRule structure for JSON output
// firewall-rules
// gcloud compute firewall-rules list - `terragoat-dev-firewall`
// forwarding rules (load balancers)
// gcloud compute forwarding-rules list
// Router & NAT gateways
// gcloud compute routers list
// gcloud compute routers nats list --router
// Global network firewall policy
// gcloud compute network-firewall-policies
// gcloud compute network-firewall-policies rules list
// gcloud compute network-firewall-policies associations list
type FirewallRule struct {
FWName string `json:"fw_name"`
Description string `json:"description"`
Direction string `json:"direction"`
Allowed map[string][]string `json:"allowed"`
Denied map[string][]string `json:"denied"`
SourceRanges []string `json:"source_ranges"`
TargetTag string `json:"target_tag"`
AllowedTraffic Traffic `json:"allowed_traffic"`
ExposedEndpoints []Endpoint `json:"exposed_endpoints"`
DestinationRanges []string `json:"destinatioRanges"`
TargetTags []string `json:"targetTags"`
}
type Traffic struct {
TCP []string `json:"tcp"`
UDP []string `json:"udp"`
}
type Endpoint struct {
IP string `json:"ip"`
Ports []string `json:"ports"`
Protocol string `json:"protocol"`
}
type NetwworkService struct {
// DataStoreService datastoreservice.DataStoreService
}
func New() *NetwworkService {
return &NetwworkService{}
}
// Returns firewall rules for a project.
func (ns *NetwworkService) FirewallRules(projectID string) ([]*compute.Firewall, error) {
ctx := context.Background()
computeService, err := compute.NewService(ctx)
if err != nil {
return nil, err
}
firewallList, err := computeService.Firewalls.List(projectID).Do()
if err != nil {
return nil, err
}
return firewallList.Items, nil
}
// Returns firewall rules with additional logic to tag rules that expose resources to the public
func (ns *NetwworkService) FirewallRulesWithPublicExposure(projectID string) ([]FirewallRule, error) {
var results []FirewallRule
log.Printf("Retrieving firewall rules for project %s", projectID)
firewalls, err := ns.FirewallRules(projectID)
log.Print("Done")
if err != nil {
log.Printf("Error fetching firewall rules for project %s: %v", projectID, err)
return nil, err
}
log.Print("Parsing firewall rules")
for _, fw := range firewalls {
parsedRule, err := parseFirewallRule(fw, projectID)
if err != nil {
log.Printf("Error parsing firewall rule for project %s: %v", projectID, err)
continue
}
results = append(results, parsedRule)
}
log.Print("Done")
return results, nil
}
// Returns a list of IPs that are mapped to a given tag. Uses a list of instances
func getIPAddressesForTargetTag(instances []ComputeEngineService.ComputeEngineInfo, tag string) ([]string, error) {
var ips []string
for _, instance := range instances {
if contains(instance.Tags.Items, tag) {
ips = append(ips, instance.NetworkInterfaces[0].NetworkIP)
}
}
return ips, nil
}
// Returns the zone from a GCP URL string with the zone in it
func getZoneNameFromURL(zoneURL string) string {
splits := strings.Split(zoneURL, "/")
return splits[len(splits)-1]
}
// Returns true of a string is in the given list of strings. Else false
func contains(slice []string, item string) bool {
for _, a := range slice {
if a == item {
return true
}
}
return false
}
// Returns true if a string in compute.Firewall.SourceRanges is "0.0.0.0/0"
func isExposedToInternet(fw *compute.Firewall) bool {
for _, rangeVal := range fw.SourceRanges {
if rangeVal == "0.0.0.0/0" {
return true
}
}
return false
}
// Returns true if the IP is an internal ipv4 or ipv6 IP
func IsInternalIP(cidr string) bool {
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
// Try parsing as just an IP (not CIDR)
ip = net.ParseIP(cidr)
if ip == nil {
log.Printf("Error parsing input %s\n", cidr)
return false
}
}
// Check for IPv4 private ranges
if ip.To4() != nil {
for _, privateCIDR := range []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
} {
_, block, _ := net.ParseCIDR(privateCIDR)
if block.Contains(ip) {
return true
}
}
return false
}
// Check for IPv6 ULA range fc00::/7
// This checks if the first byte of the address is in the range 0xfc to 0xfd
if ip[0] == 0xfc || ip[0] == 0xfd {
return true
}
return false
}
// getPublicEndpoints fetches the endpoints exposed to the internet
// and, if a TargetTag is provided, gets the IP associated with it.
func getPublicEndpoints(fw *compute.Firewall, projectID string) ([]Endpoint, error) {
var exposedEndpoints []Endpoint
ces := ComputeEngineService.New()
if isExposedToInternet(fw) {
for _, allowed := range fw.Allowed {
// Handle destination ranges
for _, destRange := range fw.DestinationRanges {
if !IsInternalIP(destRange) {
exposed := Endpoint{
IP: destRange,
Ports: allowed.Ports,
Protocol: allowed.IPProtocol,
}
exposedEndpoints = append(exposedEndpoints, exposed)
}
}
// If TargetTag is provided, fetch the associated IP addresses.
if len(fw.TargetTags) > 0 {
log.Printf("Target tags found, getting their IP...")
instances, err := ces.Instances(projectID)
if err != nil {
log.Printf("Error retrieving instances: %v", err)
return nil, err
}
for _, tag := range fw.TargetTags {
ips, err := getIPAddressesForTargetTag(instances, tag)
if err != nil {
return nil, err
}
for _, ip := range ips {
if !IsInternalIP(ip) {
exposed := Endpoint{
IP: ip,
Ports: allowed.Ports,
Protocol: allowed.IPProtocol,
}
exposedEndpoints = append(exposedEndpoints, exposed)
}
}
}
}
}
}
return exposedEndpoints, nil
}
// The original parseFirewallRule function
func parseFirewallRule(fw *compute.Firewall, projectID string) (FirewallRule, error) {
var exposedEndpoints []Endpoint
trafficAllowed := make(map[string][]string)
trafficDenied := make(map[string][]string)
for _, allowed := range fw.Allowed {
trafficAllowed[allowed.IPProtocol] = append(trafficAllowed[allowed.IPProtocol], allowed.Ports...)
}
exposedEndpoints, err := getPublicEndpoints(fw, projectID)
if err != nil {
return FirewallRule{}, err
}
for _, denied := range fw.Denied {
trafficDenied[denied.IPProtocol] = append(trafficDenied[denied.IPProtocol], denied.Ports...)
}
return FirewallRule{
FWName: fw.Name,
Description: fw.Description,
Direction: fw.Direction,
Allowed: trafficAllowed,
Denied: trafficDenied,
SourceRanges: fw.SourceRanges,
TargetTag: strings.Join(fw.TargetTags, ","),
ExposedEndpoints: exposedEndpoints,
}, nil
}
// TODO
// func (ns *NetworkService) ForwardingRules() {}
@@ -0,0 +1,35 @@
package networkservice_test
import (
"testing"
networkservice "github.com/BishopFox/cloudfox/gcp/services/networkService"
)
func TestIsInternalIP(t *testing.T) {
tests := []struct {
input string
expect bool
}{
// IPv4 tests
{"10.1.2.3", true}, // falls within 10.0.0.0/8
{"172.17.0.1", true}, // falls within 172.16.0.0/12
{"192.168.10.1", true}, // falls within 192.168.0.0/16
{"11.1.2.3", false}, // public IP
{"172.32.0.1", false}, // public IP
{"192.169.0.1", false}, // public IP
{"8.8.8.8", false}, // public IP
// IPv6 tests
{"fc00::1", true}, // falls within fc00::/7
{"fd00::1", true}, // falls within fd00::/8
{"fe80::1", false}, // link-local address, not ULA
{"2001:0db8:85a3::8a2e", false}, // public IP
}
for _, test := range tests {
result := networkservice.IsInternalIP(test.input)
if result != test.expect {
t.Errorf("IsInternalIP(%s) = %v; want %v", test.input, result, test.expect)
}
}
}
+84
View File
@@ -0,0 +1,84 @@
package oauthservice
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"golang.org/x/oauth2/google"
)
// Principal struct modified to map JSON response fields.
type Principal struct {
Email string `json:"email"`
Scope []string
}
// Temporary struct to match the JSON structure.
type tokenInfoResponse struct {
Email string `json:"email"`
Scope string `json:"scope"` // Keep as string to match JSON.
}
// OAuthService struct definition (unchanged)
type OAuthService struct {
// This struct can be expanded in the future if necessary.
}
// NewOAuthService creates a new instance of OAuthService.
func NewOAuthService() *OAuthService {
return &OAuthService{}
}
// WhoAmI function returns the Principal struct showing the email and scope of the currently authenticated user by generating an oauth token
func (s *OAuthService) WhoAmI() (*Principal, error) {
ctx := context.Background()
ts, err := google.DefaultTokenSource(ctx, "")
if err != nil {
return nil, fmt.Errorf("failed to obtain default token source: %v", err)
}
token, err := ts.Token()
if err != nil {
return nil, fmt.Errorf("failed to get token from token source: %v", err)
}
tokenInfo, err := queryTokenInfo(token.AccessToken)
if err != nil {
return nil, fmt.Errorf(fmt.Sprintf("failed to retrieve metada of the token with error: %s", err.Error()))
}
// Split the scope string into a slice of strings.
scopes := strings.Split(tokenInfo.Scope, " ")
return &Principal{
Email: tokenInfo.Email,
Scope: scopes,
}, nil
}
// queryTokenInfo function modified to return (*Principal, error).
func queryTokenInfo(accessToken string) (*tokenInfoResponse, error) {
url := fmt.Sprintf("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s", accessToken)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("error making request to tokeninfo endpoint: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading tokeninfo response body: %v", err)
}
var response tokenInfoResponse
// Unmarshal the JSON response body into the Principal struct.
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("error unmarshalling tokeninfo response body: %v", err)
}
return &response, nil
}
@@ -0,0 +1,83 @@
package secretservice
import (
"context"
"fmt"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
)
// Wrappers and abstracting types to facilitate mocking the client responses
type Iterator interface {
Next() (*secretmanagerpb.Secret, error)
}
type SecretsManagerClientWrapper struct {
Closer func() error
SecretLister func(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) Iterator
}
func (w *SecretsManagerClientWrapper) Close() error {
return w.Closer()
}
func (w *SecretsManagerClientWrapper) ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) Iterator {
return w.SecretLister(ctx, req, opts...)
}
type SecretsService struct {
Client *SecretsManagerClientWrapper
}
// New function to facilitate using the ss client
func New(client *secretmanager.Client) SecretsService {
ss := SecretsService{
Client: &SecretsManagerClientWrapper{
Closer: client.Close,
SecretLister: func(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) Iterator {
return client.ListSecrets(ctx, req, opts...)
},
},
}
return ss
}
type SecretInfo struct {
Name string `json:"name"`
ProjectID string `json:"projectID"`
CreationTime string `json:"creationTime"`
Labels map[string]string `json:"labels"`
Rotation string `json:"rotation,omitempty"`
}
func (ss *SecretsService) Secrets(projectID string) ([]SecretInfo, error) {
var secrets []SecretInfo
req := &secretmanagerpb.ListSecretsRequest{
Parent: fmt.Sprintf("projects/%s", projectID),
}
ctx := context.Background()
it := ss.Client.ListSecrets(ctx, req)
for {
resp, err := it.Next() //Here it errors out
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list secrets: %v", err)
}
secrets = append(secrets, SecretInfo{
Name: resp.Name,
ProjectID: projectID,
CreationTime: resp.CreateTime.AsTime().String(),
Labels: resp.Labels,
Rotation: resp.Rotation.String(),
})
}
return secrets, nil
}
@@ -0,0 +1,157 @@
package secretservice_test
import (
"context"
"reflect"
"testing"
"time"
secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
secretservice "github.com/BishopFox/cloudfox/gcp/services/secretsService"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
"google.golang.org/protobuf/types/known/timestamppb"
)
type mockSecretManagerClient struct {
secrets []*secretmanagerpb.Secret
}
func (m *mockSecretManagerClient) Close() error {
return nil
}
// SecretIterator simulates the iterator over secrets.
type SecretIterator struct {
secrets []*secretmanagerpb.Secret // Current secrets batch
nextIndex int // Index of the next secret to return
InternalFetch func(pageSize int, pageToken string) (results []*secretmanagerpb.Secret, nextPageToken string, err error)
currentPageToken string
}
func (it *SecretIterator) Next() (*secretmanagerpb.Secret, error) {
// If we have reached the end of the current secrets batch, attempt to fetch the next batch
if it.nextIndex >= len(it.secrets) {
var err error
it.secrets, it.currentPageToken, err = it.InternalFetch(10, it.currentPageToken) // Assuming a fixed page size for simplicity
if err != nil {
return nil, err // Propagate errors from fetch
}
it.nextIndex = 0 // Reset the index for the new batch
// If the fetched batch is empty, we have reached the end of the iteration
if len(it.secrets) == 0 {
return nil, iterator.Done
}
}
// Retrieve the next secret and increment the index
secret := it.secrets[it.nextIndex]
it.nextIndex++
return secret, nil
}
// ListSecrets simulates the ListSecrets method of the SecretManagerClient interface.
func (m *mockSecretManagerClient) ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) secretservice.Iterator {
it := &SecretIterator{}
// Copy secrets to avoid modifying the original slice
allSecrets := make([]*secretmanagerpb.Secret, len(m.secrets))
copy(allSecrets, m.secrets)
// Simulate internal fetch by setting InternalFetch to a function that returns the next set of secrets
nextIndex := 0
it.InternalFetch = func(pageSize int, pageToken string) (results []*secretmanagerpb.Secret, nextPageToken string, err error) {
// Check if we have reached the end of the secrets slice
if nextIndex >= len(allSecrets) {
return nil, "", iterator.Done
}
// Determine the slice of secrets to return
endIndex := nextIndex + pageSize
if endIndex > len(allSecrets) {
endIndex = len(allSecrets)
}
results = allSecrets[nextIndex:endIndex]
nextIndex = endIndex
// Simulate the nextPageToken logic (omitted for brevity, always return empty string in this mock)
nextPageToken = ""
return results, nextPageToken, nil
}
return it
}
func TestSecrets(t *testing.T) {
mockClient := &mockSecretManagerClient{}
ss := secretservice.SecretsService{
Client: &secretservice.SecretsManagerClientWrapper{
Closer: mockClient.Close,
SecretLister: func(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) secretservice.Iterator {
return mockClient.ListSecrets(ctx, req, opts...)
},
},
}
tests := []struct {
name string
projectID string
secrets []*secretmanagerpb.Secret
want []secretservice.SecretInfo
wantErr bool
}{
{
name: "Retrieve secrets successfully",
projectID: "my-project",
secrets: []*secretmanagerpb.Secret{
{
Name: "projects/my-project/secrets/secret1",
Labels: map[string]string{"env": "test"},
CreateTime: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
},
want: []secretservice.SecretInfo{
{
Name: "projects/my-project/secrets/secret1",
ProjectID: "my-project",
CreationTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).String(),
Labels: map[string]string{"env": "test"},
},
},
wantErr: false,
},
{
name: "No secrets found",
projectID: "empty-project",
secrets: []*secretmanagerpb.Secret{},
want: []secretservice.SecretInfo{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient.secrets = tt.secrets
got, err := ss.Secrets(tt.projectID) // Adapt this call to match the actual method signature
if (err != nil) != tt.wantErr {
t.Errorf("[%s] Secrets() error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
}
if len(got) != len(tt.want) {
t.Errorf("[%s] Secrets() got length = %v, want length %v", tt.name, len(got), len(tt.want))
return
}
for i, g := range got {
w := tt.want[i]
// Compare Name, Labels, and CreationTime as string
if g.Name != w.Name || !reflect.DeepEqual(g.Labels, w.Labels) || g.CreationTime != w.CreationTime {
t.Errorf("Secrets() got = %v, want %v", g, w)
}
}
})
}
}
+17
View File
@@ -0,0 +1,17 @@
package globals
// Module names
// const GCP_WHOAMI_MODULE_NAME = "whoami"
const GCP_ARTIFACT_RESGISTRY_MODULE_NAME string = "artifact-registry"
const GCP_BIGQUERY_MODULE_NAME string = "bigquery"
const GCP_BUCKETS_MODULE_NAME string = "buckets"
const GCP_INSTANCES_MODULE_NAME string = "instances"
const GCP_IAM_MODULE_NAME string = "iam"
const GCP_SECRETS_MODULE_NAME string = "secrets"
const GCP_WHOAMI_MODULE_NAME string = "whoami"
// const GCP_INVENTORY_MODULE_NAME string = "inventory"
// const GCP_GCLOUD_REFRESH_TOKENS_DB_PATH = ".config/gcloud/credentials.db"
// const GCP_GCLOUD_ACCESS_TOKENS_DB_PATH = ".config/gcloud/access_tokens.db"
// const GCP_GCLOUD_DEFAULT_CONFIG_PATH = ".config/gcloud/configurations/config_default"
// const GCP_GCLOUD_APPLICATION_DEFAULT_PATH = ".config/gcloud/application_default_credentials.json"
+1
View File
@@ -4,3 +4,4 @@ const CLOUDFOX_USER_AGENT = "cloudfox"
const CLOUDFOX_LOG_FILE_DIR_NAME = ".cloudfox"
const CLOUDFOX_BASE_DIRECTORY = "cloudfox-output"
const LOOT_DIRECTORY_NAME = "loot"
const CLOUDFOX_VERSION = "1.14.0"
+150 -85
View File
@@ -1,73 +1,120 @@
module github.com/BishopFox/cloudfox
go 1.20
go 1.21.2
toolchain go1.21.6
require (
cloud.google.com/go/artifactregistry v1.14.6
cloud.google.com/go/bigquery v1.57.1
cloud.google.com/go/iam v1.1.5
cloud.google.com/go/resourcemanager v1.9.4
cloud.google.com/go/secretmanager v1.11.4
cloud.google.com/go/storage v1.35.1
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1
github.com/Azure/go-autorest/autorest v0.11.29
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12
github.com/aquasecurity/table v1.8.0
github.com/aws/aws-sdk-go-v2 v1.21.2
github.com/aws/aws-sdk-go-v2/config v1.19.1
github.com/aws/aws-sdk-go-v2/service/apigateway v1.18.2
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.15.0
github.com/aws/aws-sdk-go-v2/service/apprunner v1.21.2
github.com/aws/aws-sdk-go-v2/service/cloud9 v1.18.11
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.36.0
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.28.7
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.29.2
github.com/aws/aws-sdk-go-v2/service/codebuild v1.22.2
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.23.0
github.com/aws/aws-sdk-go-v2/service/ec2 v1.127.0
github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2
github.com/aws/aws-sdk-go-v2/service/ecs v1.30.4
github.com/aws/aws-sdk-go-v2/service/efs v1.21.9
github.com/aws/aws-sdk-go-v2/service/eks v1.30.0
github.com/aws/aws-sdk-go-v2/service/elasticache v1.30.0
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.18.0
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.22.0
github.com/aws/aws-sdk-go-v2/service/fsx v1.34.0
github.com/aws/aws-sdk-go-v2/service/glue v1.64.0
github.com/aws/aws-sdk-go-v2/service/grafana v1.15.7
github.com/aws/aws-sdk-go-v2/service/iam v1.23.0
github.com/aws/aws-sdk-go-v2/service/lambda v1.41.0
github.com/aws/aws-sdk-go-v2/service/lightsail v1.29.0
github.com/aws/aws-sdk-go-v2/service/mq v1.17.0
github.com/aws/aws-sdk-go-v2/service/opensearch v1.22.0
github.com/aws/aws-sdk-go-v2/service/organizations v1.20.8
github.com/aws/aws-sdk-go-v2/service/ram v1.20.7
github.com/aws/aws-sdk-go-v2/service/rds v1.58.0
github.com/aws/aws-sdk-go-v2/service/redshift v1.31.0
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.16.2
github.com/aws/aws-sdk-go-v2/service/route53 v1.31.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.112.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.6
github.com/aws/aws-sdk-go-v2/service/sns v1.22.2
github.com/aws/aws-sdk-go-v2/service/sqs v1.24.7
github.com/aws/aws-sdk-go-v2/service/ssm v1.40.0
github.com/aws/aws-sdk-go-v2/service/sts v1.23.2
github.com/aws/smithy-go v1.15.0
github.com/bishopfox/awsservicemap v1.0.2
github.com/aws/aws-sdk-go-v2 v1.26.1
github.com/aws/aws-sdk-go-v2/config v1.26.2
github.com/aws/aws-sdk-go-v2/credentials v1.16.13
github.com/aws/aws-sdk-go-v2/service/apigateway v1.21.6
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.18.6
github.com/aws/aws-sdk-go-v2/service/apprunner v1.25.5
github.com/aws/aws-sdk-go-v2/service/athena v1.37.3
github.com/aws/aws-sdk-go-v2/service/cloud9 v1.22.5
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.42.5
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.32.5
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.35.6
github.com/aws/aws-sdk-go-v2/service/codeartifact v1.23.5
github.com/aws/aws-sdk-go-v2/service/codebuild v1.26.5
github.com/aws/aws-sdk-go-v2/service/codecommit v1.20.0
github.com/aws/aws-sdk-go-v2/service/codedeploy v1.22.2
github.com/aws/aws-sdk-go-v2/service/datapipeline v1.19.5
github.com/aws/aws-sdk-go-v2/service/directoryservice v1.24.4
github.com/aws/aws-sdk-go-v2/service/docdb v1.29.5
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.7
github.com/aws/aws-sdk-go-v2/service/ec2 v1.142.0
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.6
github.com/aws/aws-sdk-go-v2/service/ecs v1.35.6
github.com/aws/aws-sdk-go-v2/service/efs v1.26.5
github.com/aws/aws-sdk-go-v2/service/eks v1.37.0
github.com/aws/aws-sdk-go-v2/service/elasticache v1.34.6
github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk v1.20.5
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.21.6
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.26.6
github.com/aws/aws-sdk-go-v2/service/emr v1.35.6
github.com/aws/aws-sdk-go-v2/service/fsx v1.40.0
github.com/aws/aws-sdk-go-v2/service/glue v1.73.0
github.com/aws/aws-sdk-go-v2/service/grafana v1.18.5
github.com/aws/aws-sdk-go-v2/service/iam v1.28.5
github.com/aws/aws-sdk-go-v2/service/kinesis v1.24.6
github.com/aws/aws-sdk-go-v2/service/lambda v1.49.6
github.com/aws/aws-sdk-go-v2/service/lightsail v1.32.5
github.com/aws/aws-sdk-go-v2/service/mq v1.20.6
github.com/aws/aws-sdk-go-v2/service/opensearch v1.27.1
github.com/aws/aws-sdk-go-v2/service/organizations v1.23.5
github.com/aws/aws-sdk-go-v2/service/ram v1.23.7
github.com/aws/aws-sdk-go-v2/service/rds v1.66.0
github.com/aws/aws-sdk-go-v2/service/redshift v1.39.7
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.19.6
github.com/aws/aws-sdk-go-v2/service/route53 v1.36.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.7
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.122.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.0
github.com/aws/aws-sdk-go-v2/service/sfn v1.24.6
github.com/aws/aws-sdk-go-v2/service/sns v1.26.6
github.com/aws/aws-sdk-go-v2/service/sqs v1.29.6
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.6
github.com/aws/aws-sdk-go-v2/service/sts v1.26.6
github.com/aws/smithy-go v1.20.2
github.com/bishopfox/awsservicemap v1.0.3
github.com/bishopfox/knownawsaccountslookup v0.0.0-20231228165844-c37ef8df33cb
github.com/dominikbraun/graph v0.23.0
github.com/fatih/color v1.15.0
github.com/fatih/color v1.16.0
github.com/goccy/go-json v0.10.2
github.com/googleapis/gax-go/v2 v2.12.0
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.10.0
github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.14.0
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0
golang.org/x/crypto v0.17.0
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b
)
require github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
golang.org/x/sync v0.5.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.4.0 // indirect
golang.org/x/oauth2 v0.15.0
google.golang.org/api v0.152.0
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17
google.golang.org/protobuf v1.33.0
)
require (
cloud.google.com/go v0.110.10 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/longrunning v0.5.4 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
@@ -77,51 +124,69 @@ require (
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/apache/arrow/go/v12 v12.0.0 // indirect
github.com/apache/thrift v0.16.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6 // indirect
github.com/aws/aws-sdk-go-v2/service/athena v1.32.0
github.com/aws/aws-sdk-go-v2/service/codeartifact v1.20.2
github.com/aws/aws-sdk-go-v2/service/codecommit v1.16.4
github.com/aws/aws-sdk-go-v2/service/codedeploy v1.18.3
github.com/aws/aws-sdk-go-v2/service/datapipeline v1.16.2
github.com/aws/aws-sdk-go-v2/service/docdb v1.24.0
github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk v1.17.2
github.com/aws/aws-sdk-go-v2/service/emr v1.29.0
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.37 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6 // indirect
github.com/aws/aws-sdk-go-v2/service/kinesis v1.20.0
github.com/aws/aws-sdk-go-v2/service/sfn v1.19.8
github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/go-openapi/errors v0.20.4 // indirect
github.com/go-openapi/strfmt v0.21.7 // indirect
github.com/go-openapi/errors v0.21.0 // indirect
github.com/go-openapi/strfmt v0.21.10 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v2.0.8+incompatible // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/asmfmt v1.3.2 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/neo4j/neo4j-go-driver/v5 v5.14.0
github.com/oklog/ulid v1.3.1 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.mongodb.org/mongo-driver v1.12.1 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
)
+320 -509
View File
File diff suppressed because it is too large. Load diff
+119 -38
View File
@@ -3,8 +3,11 @@ package internal
import (
"bufio"
"context"
"encoding/gob"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
@@ -18,6 +21,7 @@ import (
"github.com/aws/smithy-go/ptr"
"github.com/bishopfox/awsservicemap"
"github.com/kyokomi/emoji"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
)
@@ -30,11 +34,95 @@ var (
ConfigMap = map[string]aws.Config{}
)
type CloudFoxRunData struct {
Profile string
AccountID string
OutputLocation string
}
func init() {
gob.Register(aws.Config{})
gob.Register(sts.GetCallerIdentityOutput{})
gob.Register(CloudFoxRunData{})
}
func InitializeCloudFoxRunData(AWSProfile string, version string, AwsMfaToken string, AWSOutputDirectory string) (CloudFoxRunData, error) {
var runData CloudFoxRunData
cacheDirectory := filepath.Join(AWSOutputDirectory, "cached-data", "aws")
filename := filepath.Join(cacheDirectory, fmt.Sprintf("CloudFoxRunData-%s.json", AWSProfile))
if _, err := os.Stat(filename); err == nil {
// unmarshall the data from the file into type CloudFoxRunData
// Open the file (this is not actually needed if you use os.ReadFile, so you can skip this)
file, err := os.Open(filename)
if err != nil {
return CloudFoxRunData{}, err
}
defer file.Close()
// Read the file content
jsonData, err := os.ReadFile(filename)
if err != nil {
return CloudFoxRunData{}, err
}
// Unmarshal jsonData into runData (make sure to pass a pointer to runData)
err = json.Unmarshal(jsonData, &runData)
if err != nil {
return CloudFoxRunData{}, err
}
return runData, nil
}
CallerIdentity, err := AWSWhoami(AWSProfile, version, AwsMfaToken)
if err != nil {
return CloudFoxRunData{}, err
}
outputLocation := filepath.Join(AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", AWSProfile, ptr.ToString(CallerIdentity.Account)))
runData = CloudFoxRunData{
Profile: AWSProfile,
AccountID: aws.ToString(CallerIdentity.Account),
OutputLocation: outputLocation,
}
// Marshall the data to a file
err = os.MkdirAll(cacheDirectory, 0755)
if err != nil {
return CloudFoxRunData{}, err
}
file, err := os.Create(filename)
if err != nil {
return CloudFoxRunData{}, err
}
defer file.Close()
jsonData, err := json.Marshal(runData)
if err != nil {
return CloudFoxRunData{}, err
}
_, err = file.Write(jsonData)
if err != nil {
return CloudFoxRunData{}, err
}
return runData, nil
}
func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string) aws.Config {
// Loads the AWS config file and returns a config object
var cfg aws.Config
var err error
// cacheKey := fmt.Sprintf("AWSConfigFileLoader-%s", AWSProfile)
// cached, found := Cache.Get(cacheKey)
// if found {
// cfg = cached.(aws.Config)
// return cfg
// }
// Check if the profile is already in the config map. If not, load it and retrieve the credentials. If it is, return the cached config object
// The AssumeRoleOptions below are used to pass the MFA token to the AssumeRole call (when applicable)
if _, ok := ConfigMap[AWSProfile]; !ok {
@@ -60,20 +148,28 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string)
}
if err != nil {
fmt.Println(err)
TxtLog.Println(err)
//fmt.Println(err)
if AWSProfile != "" {
TxtLog.Println(err)
fmt.Printf("[%s][%s] The specified profile [%s] does not exist or there was an error loading the credentials.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile), AWSProfile)
TxtLog.Fatalf("Could not retrieve the specified profile name %s", err)
} else {
fmt.Printf("[%s][%s] Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile))
TxtLog.Fatalf("Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile))
}
//os.Exit(1)
}
_, err := cfg.Credentials.Retrieve(context.TODO())
if err != nil {
fmt.Printf("[%s][%s] Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile))
TxtLog.Printf("Could not retrieve the specified profile name %s", err)
} else {
// update the config map with the new config for future lookups
ConfigMap[AWSProfile] = cfg
//return the config object for this first iteration
//Cache.Set(cacheKey, cfg, cache.DefaultExpiration)
return cfg
}
@@ -82,10 +178,21 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string)
cfg = ConfigMap[AWSProfile]
return cfg
}
//Cache.Set(cacheKey, cfg, cache.DefaultExpiration)
return cfg
}
func AWSWhoami(awsProfile string, version string, AwsMfaToken string) (*sts.GetCallerIdentityOutput, error) {
cacheKey := fmt.Sprintf("sts-getCallerIdentity-%s", awsProfile)
if cached, found := Cache.Get(cacheKey); found {
// Correct type assertion: assert the type, not a variable.
if cachedValue, ok := cached.(*sts.GetCallerIdentityOutput); ok {
return cachedValue, nil
}
// Handle the case where type assertion fails, if necessary.
}
// Connects to STS and checks caller identity. Same as running "aws sts get-caller-identity"
//fmt.Printf("[%s] Retrieving caller's identity\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)))
STSService := sts.NewFromConfig(AWSConfigFileLoader(awsProfile, version, AwsMfaToken))
@@ -96,10 +203,18 @@ func AWSWhoami(awsProfile string, version string, AwsMfaToken string) (*sts.GetC
return CallerIdentity, err
}
// Convert CallerIdentity to something i can store using the cache
Cache.Set(cacheKey, CallerIdentity, cache.DefaultExpiration)
return CallerIdentity, err
}
func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) []string {
cacheKey := fmt.Sprintf("GetEnabledRegions-%s", awsProfile)
cached, found := Cache.Get(cacheKey)
if found {
return cached.([]string)
}
var enabledRegions []string
ec2Client := ec2.NewFromConfig(ConfigMap[awsProfile])
regions, err := ec2Client.DescribeRegions(
@@ -123,45 +238,11 @@ func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) []
for _, region := range regions.Regions {
enabledRegions = append(enabledRegions, *region.RegionName)
}
Cache.Set(cacheKey, enabledRegions, cache.DefaultExpiration)
return enabledRegions
}
// func GetRegionsForService(awsProfile string, service string) []string {
// SSMClient := ssm.NewFromConfig(AWSConfigFileLoader(awsProfile))
// var PaginationControl *string
// var supportedRegions []string
// path := fmt.Sprintf("/aws/service/global-infrastructure/services/%s/regions", service)
// ServiceRegions, err := SSMClient.GetParametersByPath(
// context.TODO(),
// &(ssm.GetParametersByPathInput{
// NextToken: PaginationControl,
// Path: &path,
// }),
// )
// if err != nil {
// fmt.Println(err.Error())
// }
// if ServiceRegions.Parameters != nil {
// for _, region := range ServiceRegions.Parameters {
// name := *region.Value
// supportedRegions = append(supportedRegions, name)
// }
// // The "NextToken" value is nil when there's no more data to return.
// if ServiceRegions.NextToken != nil {
// PaginationControl = ServiceRegions.NextToken
// } else {
// PaginationControl = nil
// }
// }
// return supportedRegions
// }
// txtLogger - Returns the txt logger
func TxtLogger() *logrus.Logger {
var txtFile *os.File
+87
View File
@@ -3,6 +3,8 @@ package policy
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
type Policy struct {
@@ -79,3 +81,88 @@ func contains(list []string, elem string) bool {
return false
}
// source: https://github.com/nccgroup/PMapper/blob/master/principalmapper/querying/local_policy_simulation.py
func composePattern(stringToTransform string) *regexp.Regexp {
// Escape special characters and replace wildcards
escaped := strings.ReplaceAll(stringToTransform, ".", "\\.")
escaped = strings.ReplaceAll(escaped, "*", ".*")
escaped = strings.ReplaceAll(escaped, "?", ".")
escaped = strings.ReplaceAll(escaped, "$", "\\$")
escaped = strings.ReplaceAll(escaped, "^", "\\^")
// Compile the regular expression, ignoring case
pattern, err := regexp.Compile("(?i)^" + escaped + "$")
if err != nil {
panic("regexp compile error: " + err.Error())
}
return pattern
}
// source: https://github.com/nccgroup/PMapper/blob/master/principalmapper/querying/local_policy_simulation.py
// MatchesAfterExpansion checks the stringToCheck against stringToCheckAgainst.
func MatchesAfterExpansion(stringFromPolicyToCheck, stringToCheckAgainst string) bool {
// Transform the stringToCheckAgainst into a regex pattern
pattern := composePattern(stringToCheckAgainst)
// Check if the pattern matches stringToCheck
return pattern.MatchString(stringFromPolicyToCheck)
}
func (p *Policy) DoesPolicyHaveMatchingStatement(effect string, actionToCheck string, resourceToCheck string) bool {
for _, statement := range p.Statement {
if statement.Effect == effect {
matchesAction, matchesResource := false, false
for _, action := range statement.Action {
if MatchesAfterExpansion(actionToCheck, action) {
matchesAction = true
if resourceToCheck != "" {
for _, resource := range statement.Resource {
if MatchesAfterExpansion(resourceToCheck, resource) {
matchesResource = true
}
}
for _, notResource := range statement.NotResource {
if MatchesAfterExpansion(resourceToCheck, notResource) {
matchesResource = false
}
}
}
}
}
for _, notAction := range statement.NotAction {
matchesAction = true
if notAction == "*" {
matchesAction = false
}
if notAction == actionToCheck {
matchesAction = false
}
if MatchesAfterExpansion(actionToCheck, notAction) {
matchesAction = false
}
if resourceToCheck != "" {
for _, resource := range statement.Resource {
if MatchesAfterExpansion(resourceToCheck, resource) {
matchesResource = true
}
}
for _, notResource := range statement.NotResource {
if MatchesAfterExpansion(resourceToCheck, notResource) {
matchesResource = false
}
}
}
}
if matchesAction && matchesResource {
return true
}
}
}
return false
}
+72
View File
@@ -150,3 +150,75 @@ func getTestFixure(filename string) (*Policy, error) {
return &policy, nil
}
func TestDoesPolicyHaveMatchingStatement(t *testing.T) {
p := &Policy{
Statement: []PolicyStatement{
{
Effect: "Allow",
Action: []string{"ec2:*"},
Resource: []string{"*"},
},
{
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"arn:aws:s3:::bucket/*"},
},
{
Effect: "Deny",
Action: []string{"s3:*"},
Resource: []string{"arn:aws:s3:::bucket2/*"},
},
},
}
tests := []struct {
effect string
actionToCheck string
resourceToCheck string
want bool
}{
{
effect: "Allow",
actionToCheck: "ec2:DescribeInstances",
resourceToCheck: "arn:aws:ec2:us-west-2:123456789012:instance/*",
want: true,
},
{
effect: "Allow",
actionToCheck: "s3:GetObject",
resourceToCheck: "arn:aws:s3:::bucket/file.txt",
want: true,
},
{
effect: "Allow",
actionToCheck: "s3:PutObject",
resourceToCheck: "arn:aws:s3:::bucket/file.txt",
want: false,
},
{
effect: "Deny",
actionToCheck: "s3:GetObject",
resourceToCheck: "arn:aws:s3:::bucket2/file.txt",
want: true,
},
{
effect: "Deny",
actionToCheck: "s3:PutObject",
resourceToCheck: "arn:aws:s3:::bucket2/file.txt",
want: true,
},
{
effect: "Deny",
actionToCheck: "s3:GetObject",
resourceToCheck: "arn:aws:s3:::bucket/file.txt",
want: false,
},
}
for _, tt := range tests {
actual := p.DoesPolicyHaveMatchingStatement(tt.effect, tt.actionToCheck, tt.resourceToCheck)
if tt.want != actual {
t.Errorf("DoesPolicyHaveMatchingStatement(%s, %s, %s) is %v but should be %v", tt.effect, tt.actionToCheck, tt.resourceToCheck, actual, tt.want)
}
}
}
+119
View File
@@ -0,0 +1,119 @@
package policy
import (
"encoding/json"
"errors"
"net/url"
"regexp"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
)
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"`
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
OidcEksSub ListOfPrincipals `json:"OidcEksSub"`
OidcEksAud string `json:"OidcEksAud"`
CognitoAud string `json:"cognito-identity.amazonaws.com:aud"`
TerraformAud string `json:"app.terraform.io:aud"` // Terraform Cloud specific
TerraformSub ListOfPrincipals `json:"app.terraform.io:sub"` // Terraform Cloud specific
GCPAud string `json:"accounts.google.com:aud"`
GCPSub ListOfPrincipals `json:"accounts.google.com:sub"`
AzureADIss ListOfPrincipals `json:"http://sts.windows.net/tenant-id/iss"` // Azure AD specific
AzureADSub ListOfPrincipals `json:"sub"` // Common among OIDC providers
PingClientId string `json:"pingidentity.com:client_id"`
GoogleWorkspaceAud string `json:"workspace.google.com:aud"`
GoogleWorkspaceSub ListOfPrincipals `json:"workspace.google.com:sub"`
CircleCIAud ListOfPrincipals `json:"CircleCIAud"`
CircleCISub ListOfPrincipals `json:"CircleCISub"`
// Add other provider-specific claims here
} `json:"StringEquals"`
StringLike struct {
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
OidcEksSub ListOfPrincipals `json:"OidcEksSub"`
OidcEksAud string `json:"OidcEksAud"`
TerraformAud string `json:"app.terraform.io:aud"` // Terraform Cloud specific
TerraformSub ListOfPrincipals `json:"app.terraform.io:sub"` // Terraform Cloud specific
GCPAud string `json:"accounts.google.com:aud"`
GCPSub ListOfPrincipals `json:"accounts.google.com:sub"`
AzureADIss ListOfPrincipals `json:"http://sts.windows.net/tenant-id/iss"` // Azure AD specific
AzureADSub ListOfPrincipals `json:"sub"` // Common among OIDC providers
PingClientId string `json:"pingidentity.com:client_id"`
GoogleWorkspaceAud string `json:"workspace.google.com:aud"`
GoogleWorkspaceSub ListOfPrincipals `json:"workspace.google.com:sub"`
CircleCIAud ListOfPrincipals `json:"CircleCIAud"`
CircleCISub ListOfPrincipals `json:"CircleCISub"`
// Add patterns for provider-specific claims that support wildcards or partial matches
} `json:"StringLike"`
ForAnyValueStringLike struct {
CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"`
//Auth0Amr ListOfPrincipals `json:"Auth0Amr"`
// This section can be extended with any-value-like conditions for other providers if they support such conditions
} `json:"ForAnyValue:StringLike"`
} `json:"Condition"`
}
// 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")
}
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/`
//auth0Pattern := `"auth0.com\:`
circleCIPattern := `"oidc.circleci.com/org/[a-zA-Z0-9\-]+:`
var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub")
var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud")
//var reAuth0Sub = regexp.MustCompile(auth0Pattern + "amr")
var reCircleCIAud = regexp.MustCompile(circleCIPattern + "aud")
var reCircleCISSub = regexp.MustCompile(circleCIPattern + "sub")
document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub")
document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud")
//document = reAuth0Sub.ReplaceAllString(document, "\"Auth0Amr")
document = reCircleCIAud.ReplaceAllString(document, "\"CircleCIAud")
document = reCircleCISSub.ReplaceAllString(document, "\"CircleCISub")
var parsedDocumentToJSON TrustPolicyDocument
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
return parsedDocumentToJSON, nil
}
+8 -7
View File
@@ -6,13 +6,14 @@ import (
)
type PolicyStatement struct {
Sid string `json:"Sid,omitempty"`
Effect string `json:"Effect"`
Principal PolicyStatementPrincipal `json:"Principal,omitempty"`
Action ListOrString `json:"Action"`
NotAction ListOrString `json:"NotAction,omitempty"`
Resource ListOrString `json:"Resource,omitempty"`
Condition PolicyStatementCondition `json:"Condition,omitempty"`
Sid string `json:"Sid,omitempty"`
Effect string `json:"Effect"`
Principal PolicyStatementPrincipal `json:"Principal,omitempty"`
Action ListOrString `json:"Action"`
NotAction ListOrString `json:"NotAction,omitempty"`
Resource ListOrString `json:"Resource,omitempty"`
NotResource ListOrString `json:"NotResource,omitempty"`
Condition PolicyStatementCondition `json:"Condition,omitempty"`
}
func (ps *PolicyStatement) IsEmpty() bool {
+33 -1
View File
@@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/dominikbraun/graph"
"github.com/patrickmn/go-cache"
)
@@ -23,7 +24,6 @@ func SaveCacheToFiles(directory string, accountID string) error {
}
for key, item := range Cache.Items() {
// only if the key contains the accountID
if accountID != "" && strings.Contains(key, accountID) {
entry := cacheEntry{
Value: item.Object,
@@ -41,6 +41,7 @@ func SaveCacheToFiles(directory string, accountID string) error {
return err
}
}
}
return nil
}
@@ -89,6 +90,12 @@ type cacheEntry struct {
Exp int64
}
type CacheableAWSConfig struct {
Region string
//Credentials aws.CredentialsProvider
//ConfigSources []interface{}
}
func SaveCacheToGobFiles(directory string, accountID string) error {
err := os.MkdirAll(directory, 0755)
if err != nil {
@@ -96,6 +103,7 @@ func SaveCacheToGobFiles(directory string, accountID string) error {
}
for key, item := range Cache.Items() {
// only if the key contains the accountID
if accountID != "" && strings.Contains(key, accountID) {
entry := cacheEntry{
@@ -116,6 +124,7 @@ func SaveCacheToGobFiles(directory string, accountID string) error {
sharedLogger.Errorf("Could not encode the following key: %s", key)
return err
}
// }
}
}
return nil
@@ -171,3 +180,26 @@ func LoadCacheFromGobFiles(directory string) error {
//fmt.Println("Cache loaded from files.")
return nil
}
func SaveGraphToGob[K comparable, T any](directory string, name string, g *graph.Graph[K, T]) error {
err := os.MkdirAll(directory, 0755)
if err != nil {
return err
}
filename := filepath.Join(directory, name+".gob")
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
err = encoder.Encode(g)
if err != nil {
sharedLogger.Errorf("Could not encode the following graph: %s", name)
return err
}
return nil
}
+17
View File
@@ -0,0 +1,17 @@
package common
type PermissionsRow struct {
AWSService string
Type string
Name string
Arn string
PolicyType string
PolicyName string
PolicyArn string
Effect string
Action string
Resource string
Condition string
}
var PermissionRowsFromAllProfiles []PermissionsRow
+55
View File
@@ -1,6 +1,7 @@
package internal
import (
"fmt"
"log"
"os"
"os/user"
@@ -8,7 +9,10 @@ import (
"github.com/BishopFox/cloudfox/globals"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/text"
"github.com/kyokomi/emoji"
"github.com/sirupsen/logrus"
)
func init() {
@@ -30,3 +34,54 @@ func GetLogDirPath() *string {
}
return ptr.String(dir)
}
type Logger struct {
version string
txtLog *logrus.Logger
}
func NewLogger() Logger {
var logger = Logger{
version: globals.CLOUDFOX_VERSION,
txtLog: TxtLog,
}
return logger
}
func (l *Logger) Info(text string) {
l.InfoM(text, "config")
}
func (l *Logger) InfoM(text string, module string) {
var cyan = color.New(color.FgCyan).SprintFunc()
fmt.Printf("[%s][%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), cyan(module), text)
}
func (l *Logger) Success(text string) {
l.SuccessM(text, "config")
}
func (l *Logger) SuccessM(text string, module string) {
var green = color.New(color.FgGreen).SprintFunc()
fmt.Printf("[%s][%s] %s\n", green(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), green(module), text)
}
func (l *Logger) Error(text string) {
l.ErrorM(text, "config")
}
func (l *Logger) ErrorM(text string, module string) {
var red = color.New(color.FgRed).SprintFunc()
fmt.Printf("[%s][%s] %s\n", red(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), red(module), text)
l.txtLog.Printf("[%s] %s", module, text)
}
func (l *Logger) Fatal(text string) {
l.FatalM(text, "config")
}
func (l *Logger) FatalM(text string, module string) {
var red = color.New(color.FgRed).SprintFunc()
l.txtLog.Printf("[%s] %s", module, text)
fmt.Printf("[%s][%s] %s\n", red(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), red(module), text)
os.Exit(1)
}
+79 -15
View File
@@ -7,6 +7,7 @@ import (
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
@@ -38,13 +39,14 @@ type TableClient struct {
}
type TableFile struct {
Name string
TableFilePointer afero.File
CSVFilePointer afero.File
JSONFilePointer afero.File
TableCols []string
Header []string
Body [][]string
Name string
TableFilePointer afero.File
CSVFilePointer afero.File
JSONFilePointer afero.File
TableCols []string
Header []string
Body [][]string
SkipPrintToScreen bool
}
type LootClient struct {
@@ -58,6 +60,49 @@ type LootFile struct {
Contents string
}
// TODO support datastructures that enable brief or wide format
type CloudfoxOutput interface {
TableFiles() []TableFile
LootFiles() []LootFile
}
// HandleOutput dynamically handles the output based on the provided arguments.
// TODO support brief of wide
func HandleOutput(
cloudProvider string,
format string,
outputDirectory string,
verbosity int,
wrap bool,
baseCloudfoxModule string,
principal string,
resultsIdentifier string,
dataToOutput CloudfoxOutput,
) error {
// Update OutputClient fields based on arguments
outDirectoryPath := filepath.Join(outputDirectory, "cloudfox-output", cloudProvider, fmt.Sprintf("%s-%s", principal, resultsIdentifier), baseCloudfoxModule)
tables := dataToOutput.TableFiles()
lootFiles := dataToOutput.LootFiles()
outputClient := OutputClient{
Verbosity: verbosity,
CallingModule: baseCloudfoxModule,
Table: TableClient{
Wrap: wrap,
DirectoryName: outDirectoryPath,
TableFiles: tables,
},
Loot: LootClient{
DirectoryName: outDirectoryPath,
LootFiles: lootFiles,
},
}
// Handle output based on the verbosity level
outputClient.WriteFullOutput(tables, lootFiles)
return nil
}
func removeColorCodes(input string) string {
// Regular expression to match ANSI color codes
ansiRegExp := regexp.MustCompile(`\x1b\[[0-9;]*m`)
@@ -96,7 +141,7 @@ func removeColorCodesFromNestedSlice(input [][]string) [][]string {
}
func (o *OutputClient) WriteFullOutput(tables []TableFile, lootFiles []LootFile) {
logger := NewLogger()
switch o.Verbosity {
case 2:
o.Table.printTablesToScreen(tables)
@@ -126,7 +171,8 @@ func (o *OutputClient) WriteFullOutput(tables []TableFile, lootFiles []LootFile)
}
for _, path := range outputPaths {
fmt.Printf("[%s][%s] Output written to %s\n", cyan(o.CallingModule), cyan(o.PrefixIdentifier), path)
logger.InfoM(fmt.Sprintf("Output written to %s", path), o.CallingModule)
// fmt.Printf("[%s][%s] Output written to %s\n", cyan(o.CallingModule), cyan(o.PrefixIdentifier), path)
}
}
@@ -143,8 +189,11 @@ func (l *LootClient) createLootFiles(lootFiles []LootFile) {
if l.DirectoryName == "" {
l.DirectoryName = "."
}
if _, err := fileSystem.Stat(l.DirectoryName); os.IsNotExist(err) {
err = fileSystem.MkdirAll(l.DirectoryName, 0700)
lootDirectory := path.Join(l.DirectoryName, "loot")
if _, err := fileSystem.Stat(lootDirectory); os.IsNotExist(err) {
err = fileSystem.MkdirAll(lootDirectory, 0700)
if err != nil {
log.Fatal(err)
}
@@ -155,7 +204,7 @@ func (l *LootClient) createLootFiles(lootFiles []LootFile) {
l.LootFiles[i].Name = fmt.Sprintf("%s.txt", file.Name)
filePointer, err := fileSystem.OpenFile(path.Join(l.DirectoryName, l.LootFiles[i].Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
filePointer, err := fileSystem.OpenFile(path.Join(lootDirectory, l.LootFiles[i].Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatalf("error creating output file: %s", err)
}
@@ -168,9 +217,9 @@ func (l *LootClient) writeLootFiles() []string {
var fullFilePaths []string
for _, file := range l.LootFiles {
contents := []byte(file.Contents)
fullPath := path.Join(l.DirectoryName, file.Name)
err := os.WriteFile(fullPath, contents, 0644)
fullPath := path.Join(l.DirectoryName, "loot", file.Name)
err := afero.WriteFile(fileSystem, fullPath, contents, 0644) // Use Afero's WriteFile
//err := os.WriteFile(fullPath, contents, 0644)
if err != nil {
log.Fatalf("error writing loot file %s: %s", file.Name, err)
}
@@ -181,6 +230,9 @@ func (l *LootClient) writeLootFiles() []string {
func (b *TableClient) printTablesToScreen(tableFiles []TableFile) {
for _, tf := range tableFiles {
if tf.SkipPrintToScreen {
continue
}
tf.Body, tf.Header = adjustBodyForTable(tf.TableCols, tf.Header, tf.Body)
standardColumnWidth := 1000
t := table.New(os.Stdout)
@@ -455,3 +507,15 @@ func adjustBodyForTable(tableHeaders []string, fullHeaders []string, fullBody []
return adjustedBody, selectedHeaders
}
func WriteJsonlFile(file *os.File, data interface{}) error {
bytes, err := json.Marshal(data)
if err != nil {
return err
}
if _, err := file.Write(append(bytes, "\n"...)); err != nil {
return err
}
return nil
}
+3 -2
View File
@@ -4,17 +4,18 @@ import (
"os"
"github.com/BishopFox/cloudfox/cli"
"github.com/BishopFox/cloudfox/globals"
"github.com/spf13/cobra"
)
var (
rootCmd = &cobra.Command{
Use: os.Args[0],
Version: "1.12.3",
Version: globals.CLOUDFOX_VERSION,
}
)
func main() {
rootCmd.AddCommand(cli.AWSCommands, cli.AzCommands)
rootCmd.AddCommand(cli.AWSCommands, cli.AzCommands, cli.GCPCommands)
rootCmd.Execute()
}