mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-07 17:05:53 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e9d5a3b1 | ||
|
|
45cd84bcaa | ||
|
|
acabb09592 | ||
|
|
b8515157f9 | ||
|
|
57aa072dda | ||
|
|
dd688afd3e | ||
|
|
f90a52c58a | ||
|
|
cb618d29de | ||
|
|
44311b5ecb | ||
|
|
d7634a8a44 | ||
|
|
3fe4ac6afa | ||
|
|
1bb3bde996 | ||
|
|
a9553c4e68 | ||
|
|
840e3d3087 | ||
|
|
b537c62dfa | ||
|
|
c52a22c4f5 | ||
|
|
d4eb166e53 | ||
|
|
097baa072a | ||
|
|
c9daf18231 | ||
|
|
fff39851a4 | ||
|
|
63d932792b | ||
|
|
c013090792 | ||
|
|
9f0d99ab32 | ||
|
|
d92e8bb487 | ||
|
|
05bcf9c3ed | ||
|
|
376e1da619 | ||
|
|
c60c33782c | ||
|
|
c2b7a50bbd | ||
|
|
a8103f118d | ||
|
|
9b5b9fad56 | ||
|
|
0032392096 | ||
|
|
6777ef2032 | ||
|
|
8f3c24e05b | ||
|
|
529d6322f3 | ||
|
|
1c334b5da4 | ||
|
|
afbe710af9 | ||
|
|
1c12c84027 | ||
|
|
1e275d39de | ||
|
|
362fa0c91e | ||
|
|
5a6e5c1ebc | ||
|
|
91976b85e6 | ||
|
|
a5c5ec92d2 | ||
|
|
ded97a0f09 | ||
|
|
2c17ad1889 | ||
|
|
d8488e1fe1 | ||
|
|
385bf52289 | ||
|
|
2007026dbe | ||
|
|
b1dc875bec | ||
|
|
56e265bb49 | ||
|
|
4b424a3bcd | ||
|
|
0ea1895e2c | ||
|
|
6bbc528831 | ||
|
|
8ed67eadea | ||
|
|
2f518c923e | ||
|
|
17627539db | ||
|
|
3d7bf089dd | ||
|
|
5d8e9e95a7 | ||
|
|
e56634d7d5 | ||
|
|
8dc330494b | ||
|
|
d5de698d7b | ||
|
|
433f54d007 | ||
|
|
846f28e8be | ||
|
|
d455f4cde4 | ||
|
|
73207a700d | ||
|
|
97ec9f5588 | ||
|
|
3365c99559 | ||
|
|
ff04f267d2 | ||
|
|
383d28ab2c | ||
|
|
5cad2b7e28 | ||
|
|
43acb6884d | ||
|
|
66fa97ed8a | ||
|
|
ac4552d994 | ||
|
|
a0ee41fb12 | ||
|
|
56781c8303 | ||
|
|
053fc3a0c9 | ||
|
|
5b8abcd29a | ||
|
|
e9d37e9ec7 | ||
|
|
ecb41d5494 | ||
|
|
39626e44fe | ||
|
|
a61ff6bd9e | ||
|
|
c567c4f7d2 | ||
|
|
56def9b276 | ||
|
|
86dde7e600 | ||
|
|
9cba6bd4bf | ||
|
|
0f1e003ebb | ||
|
|
09e59bbd6b | ||
|
|
0ced51004d | ||
|
|
e509639aa4 | ||
|
|
8f02f3bbb2 | ||
|
|
3c4612b36e | ||
|
|
11eb8b3efb | ||
|
|
5fde076e09 | ||
|
|
9d826ded78 | ||
|
|
06029e8927 | ||
|
|
d179e8d540 | ||
|
|
13ae4b992e | ||
|
|
129a8ce76d | ||
|
|
8f76cd9581 | ||
|
|
b8608a183f | ||
|
|
ae3ac75f23 | ||
|
|
f042e07dd5 | ||
|
|
732ae09a59 | ||
|
|
1339a12329 | ||
|
|
274f16f24b | ||
|
|
b92f61d4d0 | ||
|
|
964dd5ef1c | ||
|
|
924c9dbd6e | ||
|
|
90261dd140 | ||
|
|
d460e63cea | ||
|
|
c450480810 | ||
|
|
72ce4f6575 | ||
|
|
ee151be593 | ||
|
|
0cce67728e | ||
|
|
037172cd22 | ||
|
|
68328987f7 | ||
|
|
5a71948556 | ||
|
|
8c00a82dc3 | ||
|
|
52ac0b6144 | ||
|
|
b1cff22686 | ||
|
|
d67a017bdd | ||
|
|
8fbfb38350 | ||
|
|
f2cdf4d3dc | ||
|
|
59a7bd5aaa | ||
|
|
18a500f1b7 | ||
|
|
e262dae64f | ||
|
|
4a992d3172 | ||
|
|
5e7bfb3cbd | ||
|
|
a7cccdb7ea | ||
|
|
127f2c0fff | ||
|
|
1926e2adb0 | ||
|
|
d1d335fa12 | ||
|
|
73da61eb37 | ||
|
|
7cb4ba349d | ||
|
|
e1af1b8bcd | ||
|
|
1caa02c309 | ||
|
|
3b26a03d8d | ||
|
|
63e9338a2d | ||
|
|
36740bca65 | ||
|
|
118ed625d6 | ||
|
|
530dcbdb67 | ||
|
|
79eb720e72 | ||
|
|
c3dac71f1d | ||
|
|
ecdd33c522 | ||
|
|
1c243c73dc | ||
|
|
ca7df5a039 | ||
|
|
8850384fc0 | ||
|
|
dfae1ce189 | ||
|
|
0e8febd690 | ||
|
|
775c233858 | ||
|
|
9fc38f5e64 | ||
|
|
d20b2b1099 | ||
|
|
eb95e887de | ||
|
|
7641a3ecf4 | ||
|
|
064f285652 | ||
|
|
3e0718636f | ||
|
|
b2033c0f82 | ||
|
|
d9b5c13b3b | ||
|
|
f672c34806 | ||
|
|
4d4992da33 | ||
|
|
a5d4124bb1 | ||
|
|
6123172784 | ||
|
|
0e12ca20b2 | ||
|
|
fe3a09ae4d | ||
|
|
6ace00021b | ||
|
|
5183b32d75 | ||
|
|
ffa8e09fbe | ||
|
|
26562574c3 | ||
|
|
2e82cc895d | ||
|
|
dadef384c0 | ||
|
|
e877139c76 | ||
|
|
206542faf7 | ||
|
|
5ec61019fb | ||
|
|
e919c3c9e3 | ||
|
|
61da82896b | ||
|
|
3e3a9e8745 | ||
|
|
40d7a3c2d0 | ||
|
|
8ce9a00697 | ||
|
|
7d8ebc27c7 | ||
|
|
45e548cf0b | ||
|
|
9498d2db13 | ||
|
|
71e85c29d2 | ||
|
|
5525266855 | ||
|
|
a28d4e8556 | ||
|
|
ee7c3c0387 | ||
|
|
467f6b459b | ||
|
|
cc38c1f147 | ||
|
|
bb879c34c2 | ||
|
|
00317a5336 | ||
|
|
8bfee76c28 | ||
|
|
712ec9c382 | ||
|
|
7c8afb6fef | ||
|
|
1a5f64e8c5 | ||
|
|
7c323f4f75 | ||
|
|
3db423b740 | ||
|
|
abe6f3da61 | ||
|
|
16fcbc2fdd | ||
|
|
7cafe1f066 | ||
|
|
59b082a40b | ||
|
|
b33653822c | ||
|
|
e15c71734e | ||
|
|
065e04affe | ||
|
|
26dbb56d60 | ||
|
|
58b9c7306e | ||
|
|
18f6cbe35f | ||
|
|
8cdb854740 | ||
|
|
dfe2fc1037 | ||
|
|
e35aa612ba | ||
|
|
89c6c0ddb3 | ||
|
|
6aee4780b3 | ||
|
|
351a9cc724 | ||
|
|
b3531faead | ||
|
|
7ad15cc599 | ||
|
|
95d1fbc621 | ||
|
|
91ad816b75 | ||
|
|
cd3a4be1ff | ||
|
|
b05254d434 | ||
|
|
79dbe13468 | ||
|
|
a63c62d5b1 | ||
|
|
a342c4da04 | ||
|
|
9a7ecc5a0f | ||
|
|
8dd7fba172 | ||
|
|
bc72d939f3 | ||
|
|
fe96654ae7 | ||
|
|
a8d086bf2a |
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
-1
@@ -15,6 +15,7 @@
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
.vscode
|
||||
launch.json
|
||||
|
||||
######################################################
|
||||
|
||||
@@ -61,7 +62,7 @@ terraform.rc
|
||||
|
||||
untracked/*
|
||||
output/*
|
||||
cloudfox-output/*
|
||||
*cloudfox-output*
|
||||
cloudfox
|
||||
*.log
|
||||
*.bak
|
||||
|
||||
@@ -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`
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
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,8 +39,8 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
|
||||
|
||||
| Provider| CloudFox Commands |
|
||||
| - | - |
|
||||
| AWS | 15 |
|
||||
| Azure | 2 (alpha) |
|
||||
| AWS | 23 |
|
||||
| Azure | 4 |
|
||||
| GCP | Support Planned |
|
||||
| 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)**
|
||||
|
||||
@@ -68,27 +81,43 @@ Additional policy notes (as of 09/2022):
|
||||
### Azure
|
||||
* Viewer or similar permissions applied.
|
||||
|
||||
# Supported Commands
|
||||
# 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 | [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 | [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 | [outbound-assumed-roles](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
|
||||
| AWS | [permissions](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#permissions) | Enumerates IAM permissions associated with all users and roles. Grep this output to figure out what permissions a particular principal has rather than logging into the AWS console and painstakingly expanding each policy attached to the principal you are investigating. |
|
||||
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
|
||||
| AWS | [pmapper](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#pmapper) | Looks for pmapper data stored on the local filesystem, [in the locations defined here](https://github.com/nccgroup/PMapper/wiki/Frequently-Asked-Questions#where-does-pmapper-store-its-data). If pmapper data has been found (you already ran `pmapper graph create`), then this command will use this data to build a graph in cloudfox memory let you know who can privesc to admin.
|
||||
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
|
||||
| AWS | [ram](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ram) | List all resources in this account that are shared with other accounts, or resources from other accounts that are shared with this account. Useful for cross-account attack paths. |
|
||||
| AWS | [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 | [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 |
|
||||
|
||||
# Authors
|
||||
* [Carlos Vendramini](https://github.com/carlosvendramini-bf)
|
||||
@@ -99,8 +128,6 @@ Additional policy notes (as of 09/2022):
|
||||
|
||||
# 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 +137,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.
|
||||
|
||||
+19
-16
@@ -7,8 +7,7 @@ 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/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
@@ -21,13 +20,15 @@ type AccessKeysModule struct {
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSProfile string
|
||||
OutputFormat string
|
||||
CommandCounter console.CommandCounter
|
||||
Goroutines int
|
||||
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
|
||||
}
|
||||
|
||||
@@ -41,15 +42,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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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
|
||||
@@ -74,23 +76,24 @@ 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.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
//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(outputDirectory, verbosity)
|
||||
fmt.Printf("[%s] %s access keys found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
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) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -107,7 +110,7 @@ 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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
+66
-31
@@ -10,28 +10,36 @@ import (
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"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/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type S3ListBucketsAPI interface {
|
||||
ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error)
|
||||
}
|
||||
type BucketsModule struct {
|
||||
// General configuration data
|
||||
S3Client *s3.Client
|
||||
|
||||
// This interface is used for unit testing
|
||||
S3ClientListBucketsInterface S3ListBucketsAPI
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -46,38 +54,42 @@ 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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(wg, dataReceiver)
|
||||
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
|
||||
|
||||
@@ -99,14 +111,14 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
|
||||
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(outputDirectory, verbosity, m.AWSProfile)
|
||||
fmt.Printf("[%s] %s buckets 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, m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] %s buckets found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} 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) {
|
||||
@@ -122,18 +134,18 @@ func (m *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, dataReceiver chan Bucket) {
|
||||
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) {
|
||||
defer wg.Done()
|
||||
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getBuckets(m.output.Verbosity, dataReceiver)
|
||||
wg.Add(1)
|
||||
m.createBucketsRows(m.output.Verbosity, wg, semaphore, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}
|
||||
|
||||
func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile string) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -167,29 +179,35 @@ 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, dataReceiver chan Bucket) {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
var r string = "Global"
|
||||
var name string
|
||||
ListBuckets, err := m.S3Client.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.ListBucketsInput{},
|
||||
)
|
||||
ListBuckets, err := m.listBuckets()
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, bucket := range ListBuckets.Buckets {
|
||||
for _, bucket := range ListBuckets {
|
||||
name = aws.ToString(bucket.Name)
|
||||
// Send Bucket object through the channel to the receiver
|
||||
dataReceiver <- Bucket{
|
||||
@@ -200,3 +218,20 @@ func (m *BucketsModule) getBuckets(verbosity int, dataReceiver chan Bucket) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) listBuckets() ([]types.Bucket, error) {
|
||||
|
||||
var buckets []types.Bucket
|
||||
ListBuckets, err := m.S3ClientListBucketsInterface.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.ListBucketsInput{},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return buckets, err
|
||||
}
|
||||
|
||||
buckets = append(buckets, ListBuckets.Buckets...)
|
||||
return buckets, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
type MockedS3ListBuckets struct {
|
||||
AWSRegions []string
|
||||
}
|
||||
|
||||
func (m *MockedS3ListBuckets) 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 TestListBuckets(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule BucketsModule
|
||||
expectedResult []Bucket
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: BucketsModule{
|
||||
S3ClientListBucketsInterface: &MockedS3ListBuckets{},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
OutputFormat: "table",
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
},
|
||||
expectedResult: []Bucket{{
|
||||
Name: "mockBucket123",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
internal.MockFileSystem(true)
|
||||
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 {
|
||||
if expectedBucket.Name != subtest.testModule.Buckets[index].Name {
|
||||
log.Fatal("Bucket name does not match expected name")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
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/cloudformation"
|
||||
"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 CloudFormationGetTemplateAPIClient interface {
|
||||
GetTemplate(context.Context, *cloudformation.GetTemplateInput, ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error)
|
||||
}
|
||||
type CloudformationModule struct {
|
||||
// General configuration data
|
||||
CloudFormationDescribeStacksInterface cloudformation.DescribeStacksAPIClient
|
||||
CloudFormationGetTemplateInterface CloudFormationGetTemplateAPIClient
|
||||
|
||||
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", 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)
|
||||
m.writeLoot(m.output.FilePath, 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: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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 := m.describeStacks(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 = m.getTemplateBody(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,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) describeStacks(r string) ([]types.Stack, error) {
|
||||
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var stacks []types.Stack
|
||||
|
||||
for {
|
||||
DescribeStacks, err := m.CloudFormationDescribeStacksInterface.DescribeStacks(
|
||||
context.TODO(),
|
||||
&cloudformation.DescribeStacksInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *cloudformation.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
stacks = append(stacks, DescribeStacks.Stacks...)
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if DescribeStacks.NextToken != nil {
|
||||
PaginationControl = DescribeStacks.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) getTemplateBody(r string, stackName string) (string, error) {
|
||||
|
||||
var stackTemplateBody string
|
||||
GetTemplate, err := m.CloudFormationGetTemplateInterface.GetTemplate(
|
||||
context.TODO(),
|
||||
&cloudformation.GetTemplateInput{
|
||||
StackName: &stackName,
|
||||
},
|
||||
func(o *cloudformation.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return stackTemplateBody, err
|
||||
}
|
||||
|
||||
stackTemplateBody = aws.ToString(GetTemplate.TemplateBody)
|
||||
|
||||
return stackTemplateBody, nil
|
||||
}
|
||||
@@ -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 MockedCloudformationClientDescribeStacks struct {
|
||||
describeStacks Stacks
|
||||
}
|
||||
|
||||
type MockedCloudformationClientGetTemplate struct {
|
||||
getTemplateBody TemplateBody
|
||||
}
|
||||
|
||||
func (m *MockedCloudformationClientDescribeStacks) 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 *MockedCloudformationClientGetTemplate) 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 TestCloudFormation(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule CloudformationModule
|
||||
expectedResult []CFStack
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: CloudformationModule{
|
||||
CloudFormationDescribeStacksInterface: &MockedCloudformationClientDescribeStacks{},
|
||||
CloudFormationGetTemplateInterface: &MockedCloudformationClientGetTemplate{},
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+56
-30
@@ -10,29 +10,34 @@ import (
|
||||
"strings"
|
||||
"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/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
|
||||
// These interfaces are used for unit testing
|
||||
ECRClientDescribeReposInterface ecr.DescribeRepositoriesAPIClient
|
||||
ECRClientDescribeImagesInterface ecr.DescribeImagesAPIClient
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -51,41 +56,44 @@ 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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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)
|
||||
|
||||
// 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 {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, dataReceiver)
|
||||
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
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
@@ -119,23 +127,31 @@ 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.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
m.writeLoot(outputDirectory, 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)
|
||||
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, dataReceiver chan Repository) {
|
||||
func (m *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
|
||||
defer wg.Done()
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getECRRecordsPerRegion(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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) {
|
||||
@@ -152,7 +168,7 @@ func (m *ECRModule) Receiver(receiver chan Repository, receiverDone chan bool) {
|
||||
}
|
||||
|
||||
func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -186,24 +202,34 @@ 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)
|
||||
|
||||
}
|
||||
|
||||
func (m *ECRModule) getECRRecordsPerRegion(r string, dataReceiver chan Repository) {
|
||||
func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var PaginationControl2 *string
|
||||
|
||||
for {
|
||||
DescribeRepositories, err := m.ECRClient.DescribeRepositories(
|
||||
DescribeRepositories, err := m.ECRClientDescribeReposInterface.DescribeRepositories(
|
||||
context.TODO(),
|
||||
&ecr.DescribeRepositoriesInput{
|
||||
NextToken: PaginationControl,
|
||||
@@ -225,7 +251,7 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, dataReceiver chan Repositor
|
||||
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
|
||||
var images []types.ImageDetail
|
||||
for {
|
||||
DescribeImages, err := m.ECRClient.DescribeImages(
|
||||
DescribeImages, err := m.ECRClientDescribeImagesInterface.DescribeImages(
|
||||
context.TODO(),
|
||||
&ecr.DescribeImagesInput{
|
||||
RepositoryName: &repoName,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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 MockedECRClientDescribeRepos struct {
|
||||
AWSRegions []string
|
||||
}
|
||||
|
||||
type MockedECRClientDescribeImages struct {
|
||||
AWSRegions []string
|
||||
}
|
||||
|
||||
func (m *MockedECRClientDescribeRepos) 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 *MockedECRClientDescribeImages) 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 TestDescribeRepos(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule ECRModule
|
||||
expectedResult []Repository
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: ECRModule{
|
||||
ECRClientDescribeReposInterface: &MockedECRClientDescribeRepos{},
|
||||
ECRClientDescribeImagesInterface: &MockedECRClientDescribeImages{},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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/ecs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"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
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
|
||||
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
|
||||
TaskDefinition 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.IAMSimulatePrincipalPolicyClient, 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)
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
m.output.Headers = []string{
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
for _, ecsTask := range m.MappedECSTasks {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
ecsTask.Cluster,
|
||||
ecsTask.TaskDefinition,
|
||||
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", m.AWSProfile)
|
||||
//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)
|
||||
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++
|
||||
}
|
||||
|
||||
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: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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 {
|
||||
mappedTask := MappedECSTask{
|
||||
Cluster: getNameFromARN(clusterARN),
|
||||
TaskDefinition: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
|
||||
LaunchType: string(task.LaunchType),
|
||||
ID: getIDFromECSTask(aws.ToString(task.TaskArn)),
|
||||
PrivateIP: getPrivateIPv4AddressFromECSTask(task),
|
||||
Role: m.getTaskRole(aws.ToString(task.TaskDefinitionArn), region),
|
||||
}
|
||||
|
||||
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
|
||||
if eniID != "" {
|
||||
mappedTask.ExternalIP = publicIPs[eniID]
|
||||
}
|
||||
|
||||
dataReceiver <- mappedTask
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) getTaskRole(taskDefinitionArn string, region string) string {
|
||||
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 ""
|
||||
}
|
||||
return aws.ToString(DescribeTaskDefinition.TaskDefinition.TaskRoleArn)
|
||||
}
|
||||
|
||||
/* 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 ""
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
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/eks"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"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
|
||||
EKSClientListClustersInterface eks.ListClustersAPIClient
|
||||
EKSClientDescribeClusterInterface eks.DescribeClusterAPIClient
|
||||
EKSClientListNodeGroupsInterface eks.ListNodegroupsAPIClient
|
||||
EKSClientDescribeNodeGroupInterface eks.DescribeNodegroupAPIClient
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
|
||||
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.IAMSimulatePrincipalPolicyClient, 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.
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
//"Endpoint",
|
||||
"Public",
|
||||
//"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Clusters {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
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", 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)
|
||||
m.writeLoot(m.output.FilePath, 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: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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
|
||||
var role string
|
||||
|
||||
clusters, err := m.listClusters(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, clusterName := range clusters {
|
||||
clusterDetails, err := m.describeCluster(clusterName, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
//nodeGroups = append(nodeGroups, DescribeCluster.Cluster.)
|
||||
endpoint := aws.ToString(clusterDetails.Endpoint)
|
||||
oidc := aws.ToString(clusterDetails.Identity.Oidc.Issuer)
|
||||
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 := m.listNodeGroups(clusterName, r)
|
||||
|
||||
if len(ListNodeGroups) > 0 {
|
||||
for _, nodeGroup := range ListNodeGroups {
|
||||
|
||||
nodeGroupDetails, err := m.describeNodegroup(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: "",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *EKSModule) listClusters(r string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []string
|
||||
|
||||
for {
|
||||
ListClusters, err := m.EKSClientListClustersInterface.ListClusters(
|
||||
context.TODO(),
|
||||
&eks.ListClustersInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, ListClusters.Clusters...)
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListClusters.NextToken != nil {
|
||||
PaginationControl = ListClusters.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
func (m *EKSModule) listNodeGroups(clusterName string, r string) []string {
|
||||
var PaginationControl *string
|
||||
var nodeGroups []string
|
||||
for {
|
||||
ListNodeGroups, err := m.EKSClientListNodeGroupsInterface.ListNodegroups(
|
||||
context.TODO(),
|
||||
&eks.ListNodegroupsInput{
|
||||
ClusterName: &clusterName,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
nodeGroups = append(nodeGroups, ListNodeGroups.Nodegroups...)
|
||||
if ListNodeGroups.NextToken != nil {
|
||||
PaginationControl = ListNodeGroups.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return nodeGroups
|
||||
}
|
||||
|
||||
func (m *EKSModule) describeCluster(clusterName string, r string) (*types.Cluster, error) {
|
||||
|
||||
var err error
|
||||
//var clusterDetails types.Cluster
|
||||
DescribeCluster, err := m.EKSClientDescribeClusterInterface.DescribeCluster(
|
||||
context.TODO(),
|
||||
&eks.DescribeClusterInput{
|
||||
Name: &clusterName,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
return DescribeCluster.Cluster, err
|
||||
|
||||
}
|
||||
|
||||
func (m *EKSModule) describeNodegroup(clusterName string, nodeGroup string, r string) (*types.Nodegroup, error) {
|
||||
DescribeNodegroup, err := m.EKSClientDescribeNodeGroupInterface.DescribeNodegroup(
|
||||
context.TODO(),
|
||||
&eks.DescribeNodegroupInput{
|
||||
ClusterName: &clusterName,
|
||||
NodegroupName: &nodeGroup,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
|
||||
}
|
||||
return DescribeNodegroup.Nodegroup, err
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
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 MockedEKSClientListClusters struct {
|
||||
}
|
||||
|
||||
type MockedEKSClientDescribeCluster struct {
|
||||
}
|
||||
|
||||
type MockedEKSClientListNodegroups struct {
|
||||
}
|
||||
|
||||
type MockedEKSClientDescribeNodegroup struct {
|
||||
}
|
||||
|
||||
type MockedIAMSimulatePrincipalPolicy struct {
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientListClusters) 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 *MockedEKSClientDescribeCluster) 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 *MockedEKSClientListNodegroups) 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 *MockedEKSClientDescribeNodegroup) 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{
|
||||
EKSClientListClustersInterface: &MockedEKSClientListClusters{},
|
||||
EKSClientDescribeClusterInterface: &MockedEKSClientDescribeCluster{},
|
||||
EKSClientListNodeGroupsInterface: &MockedEKSClientListNodegroups{},
|
||||
EKSClientDescribeNodeGroupInterface: &MockedEKSClientDescribeNodegroup{},
|
||||
//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")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
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", m.AWSProfile)
|
||||
//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)
|
||||
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: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+232
-154
@@ -11,8 +11,7 @@ import (
|
||||
"strings"
|
||||
"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/apigateway"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
|
||||
@@ -24,6 +23,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/grafana"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lightsail"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lightsail/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/mq"
|
||||
"github.com/aws/aws-sdk-go-v2/service/opensearch"
|
||||
"github.com/aws/aws-sdk-go-v2/service/rds"
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/aws/smithy-go"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -55,14 +56,16 @@ type EndpointsModule struct {
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Endpoints []Endpoint
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
Errors []string
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -83,29 +86,30 @@ func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory st
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "endpoints"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating endpoints for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s] Supported Services: App Runner, APIGateway, ApiGatewayV2, Cloudfront, EKS, ELB, ELBv2, Grafana, \n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s] \t\t\tLambda, MQ, OpenSearch, Redshift, RDS\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] Enumerating endpoints for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: App Runner, APIGateway, ApiGatewayV2, Cloudfront, EKS, ELB, ELBv2, Grafana, \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
fmt.Printf("[%s][%s] \t\t\tLambda, MQ, OpenSearch, Redshift, RDS\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, 50)
|
||||
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 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 Endpoint)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
//execute global checks -- removing from now. not sure i want s3 data in here
|
||||
@@ -115,6 +119,7 @@ func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory st
|
||||
go m.getCloudfrontEndpoints(wg, semaphore, dataReceiver)
|
||||
|
||||
//execute regional checks
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
@@ -127,10 +132,11 @@ func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory st
|
||||
// }
|
||||
|
||||
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
|
||||
|
||||
@@ -167,19 +173,20 @@ func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory st
|
||||
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(outputDirectory, verbosity)
|
||||
fmt.Printf("[%s] %s endpoints enumerated.\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)
|
||||
fmt.Printf("[%s][%s] %s endpoints found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s] No endpoints found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No endpoints 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)
|
||||
// This works great to print errors out after the module but i'm not really sure i want that.
|
||||
// sort.Slice(m.Errors, func(i, j int) bool {
|
||||
// return m.Errors[i] < m.Errors[j]
|
||||
// })
|
||||
// for _, e := range m.Errors {
|
||||
// fmt.Printf("[%s] %s\n", cyan(m.output.CallingModule), e)
|
||||
// fmt.Printf("[%s][%s] %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), e)
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -205,61 +212,117 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
|
||||
// <-semaphore
|
||||
// }()
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLambdaFunctionsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLambdaFunctionsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("eks", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getEksClustersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("mq", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getMqBrokersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("es", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getOpenSearchPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("grafana", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getGrafanaEndPointsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("elb", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getELBv2ListenersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getEksClustersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getELBListenersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("apigateway", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayAPIsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getMqBrokersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getOpenSearchPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getGrafanaEndPointsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getELBv2ListenersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getELBListenersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayAPIsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getRdsClustersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getRedshiftEndPointsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
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.getRedshiftEndPointsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
//apprunner is not supported by the aws json so we have to call it in every region
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAppRunnerEndpointsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLightsailContainerEndpointsPerRegion(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.getLightsailContainerEndpointsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *EndpointsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -283,12 +346,12 @@ func (m *EndpointsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Feed this endpoints into nmap and something like gowitness/aquatone for screenshots."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed this endpoints into nmap and something like gowitness/aquatone for screenshots."))
|
||||
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), f)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
|
||||
}
|
||||
|
||||
@@ -1184,7 +1247,7 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
|
||||
name := aws.ToString(instance.DBInstanceIdentifier)
|
||||
port := instance.Endpoint.Port
|
||||
endpoint := aws.ToString(instance.Endpoint.Address)
|
||||
awsService := fmt.Sprintf("RDS")
|
||||
awsService := "RDS"
|
||||
|
||||
if instance.PubliclyAccessible {
|
||||
public = "True"
|
||||
@@ -1290,80 +1353,83 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG
|
||||
|
||||
}
|
||||
|
||||
func (m *EndpointsModule) getS3EndpointsPerRegion(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
/*
|
||||
UNUSED CODE - PLEASE REVIEW AND DELETE IF IT DOESN'T APPLY
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
func (m *EndpointsModule) getS3EndpointsPerRegion(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
// 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).
|
||||
ListBuckets, _ := m.S3Client.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.ListBucketsInput{},
|
||||
)
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
|
||||
var public string
|
||||
for _, bucket := range ListBuckets.Buckets {
|
||||
name := aws.ToString(bucket.Name)
|
||||
endpoint := fmt.Sprintf("https://%s.s3.amazonaws.com", name)
|
||||
awsService := "S3"
|
||||
|
||||
var port int32 = 443
|
||||
protocol := "https"
|
||||
var r string = "Global"
|
||||
public = "False"
|
||||
|
||||
GetBucketPolicyStatus, err := m.S3Client.GetBucketPolicyStatus(
|
||||
// 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).
|
||||
ListBuckets, _ := m.S3Client.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.GetBucketPolicyStatusInput{
|
||||
Bucket: &name,
|
||||
},
|
||||
&s3.ListBucketsInput{},
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
isPublic := GetBucketPolicyStatus.PolicyStatus.IsPublic
|
||||
if isPublic {
|
||||
public = "True"
|
||||
var public string
|
||||
for _, bucket := range ListBuckets.Buckets {
|
||||
name := aws.ToString(bucket.Name)
|
||||
endpoint := fmt.Sprintf("https://%s.s3.amazonaws.com", name)
|
||||
awsService := "S3"
|
||||
|
||||
var port int32 = 443
|
||||
protocol := "https"
|
||||
var r string = "Global"
|
||||
public = "False"
|
||||
|
||||
GetBucketPolicyStatus, err := m.S3Client.GetBucketPolicyStatus(
|
||||
context.TODO(),
|
||||
&s3.GetBucketPolicyStatusInput{
|
||||
Bucket: &name,
|
||||
},
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
isPublic := GetBucketPolicyStatus.PolicyStatus.IsPublic
|
||||
if isPublic {
|
||||
public = "True"
|
||||
}
|
||||
}
|
||||
|
||||
// GetBucketWebsite, err := m.S3Client.GetBucketWebsite(
|
||||
// context.TODO(),
|
||||
// &s3.GetBucketWebsiteInput{
|
||||
// Bucket: &name,
|
||||
// },
|
||||
// )
|
||||
|
||||
// if err != nil {
|
||||
// index := *GetBucketWebsite.IndexDocument.Suffix
|
||||
// if index != "" {
|
||||
// public = "True"
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
dataReceiver <- Endpoint{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
Public: public,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// GetBucketWebsite, err := m.S3Client.GetBucketWebsite(
|
||||
// context.TODO(),
|
||||
// &s3.GetBucketWebsiteInput{
|
||||
// Bucket: &name,
|
||||
// },
|
||||
// )
|
||||
|
||||
// if err != nil {
|
||||
// index := *GetBucketWebsite.IndexDocument.Suffix
|
||||
// if index != "" {
|
||||
// public = "True"
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
dataReceiver <- Endpoint{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
Public: public,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
func (m *EndpointsModule) getCloudfrontEndpoints(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Endpoint) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
@@ -1583,6 +1649,34 @@ func (m *EndpointsModule) getLightsailContainerEndpointsPerRegion(r string, wg *
|
||||
var protocol string = "https"
|
||||
var port int32 = 443
|
||||
|
||||
containerServices, err := m.getLightsailContainerServices(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
if len(containerServices) > 0 {
|
||||
|
||||
for _, containerService := range containerServices {
|
||||
name := aws.ToString(containerService.ContainerServiceName)
|
||||
endpoint := aws.ToString(containerService.Url)
|
||||
awsService := "Lightsail [Container]"
|
||||
|
||||
dataReceiver <- Endpoint{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
Public: public,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *EndpointsModule) getLightsailContainerServices(r string) ([]types.ContainerService, error) {
|
||||
var containerServices []types.ContainerService
|
||||
GetContainerServices, err := m.LightsailClient.GetContainerServices(
|
||||
context.TODO(),
|
||||
&(lightsail.GetContainerServicesInput{}),
|
||||
@@ -1590,27 +1684,11 @@ func (m *EndpointsModule) getLightsailContainerEndpointsPerRegion(r string, wg *
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err == nil {
|
||||
|
||||
if len(GetContainerServices.ContainerServices) > 0 {
|
||||
|
||||
for _, containerService := range GetContainerServices.ContainerServices {
|
||||
name := aws.ToString(containerService.ContainerServiceName)
|
||||
//arn := *containerService.Arn
|
||||
endpoint := aws.ToString(containerService.Url)
|
||||
//endpoint := fmt.Sprintf("https//%s", *service.ServiceUrl)
|
||||
awsService := "Lightsail [Container]"
|
||||
|
||||
dataReceiver <- Endpoint{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
Public: public,
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return containerServices, err
|
||||
}
|
||||
containerServices = append(containerServices, GetContainerServices.ContainerServices...)
|
||||
return containerServices, nil
|
||||
}
|
||||
+106
-51
@@ -8,8 +8,7 @@ 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/apprunner"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs"
|
||||
@@ -19,6 +18,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"
|
||||
)
|
||||
|
||||
@@ -37,6 +37,8 @@ type EnvsModule struct {
|
||||
AWSRegions []string
|
||||
AWSProfile string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
|
||||
// Service Clients
|
||||
ECSClient *ecs.Client
|
||||
@@ -47,11 +49,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 {
|
||||
@@ -67,29 +69,31 @@ 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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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{}, 50)
|
||||
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 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 {
|
||||
@@ -100,10 +104,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
|
||||
|
||||
@@ -136,12 +141,23 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
|
||||
|
||||
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)
|
||||
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)
|
||||
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) {
|
||||
@@ -149,7 +165,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
|
||||
@@ -159,26 +177,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: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -197,13 +239,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) {
|
||||
@@ -216,32 +286,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) {
|
||||
|
||||
+45
-27
@@ -9,13 +9,13 @@ 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/efs"
|
||||
"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/bishopfox/awsservicemap"
|
||||
"github.com/fatih/color"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -31,15 +31,17 @@ type FilesystemsModule struct {
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
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
|
||||
}
|
||||
|
||||
@@ -58,45 +60,49 @@ 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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
//populate region in the filesystems module struct
|
||||
for i, region := range m.AWSRegions {
|
||||
m.Regions[i].Region = region
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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, dataReceiver)
|
||||
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
|
||||
|
||||
@@ -134,17 +140,19 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
|
||||
|
||||
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(outputDirectory, 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)
|
||||
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 {
|
||||
@@ -157,16 +165,26 @@ func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDon
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan FilesystemObject) {
|
||||
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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.getEFSSharesPerRegion(r, wg, dataReceiver)
|
||||
wg.Add(1)
|
||||
go m.getFSxSharesPerRegion(r, wg, dataReceiver)
|
||||
go m.getFSxSharesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -209,17 +227,17 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
m.modLog.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."))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup, dataReceiver chan FilesystemObject) {
|
||||
func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
@@ -322,7 +340,7 @@ func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup,
|
||||
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup, dataReceiver chan FilesystemObject) {
|
||||
func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
@@ -403,7 +421,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},
|
||||
}},
|
||||
|
||||
+75
-62
@@ -10,8 +10,7 @@ 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/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
@@ -20,18 +19,22 @@ import (
|
||||
|
||||
type IamSimulatorModule struct {
|
||||
// General configuration data
|
||||
IAMClient *iam.Client
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
IAMListUsersClient iam.ListUsersAPIClient
|
||||
IAMListRolesClient iam.ListRolesAPIClient
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -50,7 +53,7 @@ var (
|
||||
"ssm:GetParameter",
|
||||
"s3:ListBucket",
|
||||
"s3:GetObject",
|
||||
"ssm:sSendCommand",
|
||||
"ssm:SendCommand",
|
||||
"ssm:StartSession",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetAuthorizationToken",
|
||||
@@ -60,22 +63,16 @@ var (
|
||||
"apprunner:DescribeService",
|
||||
"ec2:DescribeInstanceAttributeInput",
|
||||
}
|
||||
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)
|
||||
@@ -84,56 +81,56 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
var pmapperOutFileName string
|
||||
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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)
|
||||
m.getIAMUsers(wg, actionList, resource, dataReceiver)
|
||||
wg.Add(1)
|
||||
m.getIAMRoles(wg, actionList, resource, dataReceiver)
|
||||
pmapperOutFileName = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
pmapperOutFileName = filepath.Join(m.output.FullFilename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
|
||||
} else {
|
||||
// Both --principal and --action are empty. Run in default mode!
|
||||
fmt.Printf("[%s] 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 {
|
||||
pmapperOutFileName = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
pmapperOutFileName = filepath.Join(m.output.FullFilename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
|
||||
}
|
||||
|
||||
@@ -141,10 +138,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,23 +173,24 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
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", m.AWSProfile)
|
||||
//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)
|
||||
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(outputDirectory, verbosity, pmapperCommands)
|
||||
m.writeLoot(m.output.FilePath, 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) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -212,12 +211,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)
|
||||
|
||||
}
|
||||
|
||||
@@ -252,10 +251,9 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
|
||||
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(
|
||||
ListUsers, err := m.IAMListUsersClient.ListUsers(
|
||||
context.TODO(),
|
||||
&iam.ListUsersInput{
|
||||
Marker: PaginationControl,
|
||||
@@ -270,8 +268,16 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
|
||||
for _, user := range ListUsers.Users {
|
||||
//name := user.UserName
|
||||
principal := user.Arn
|
||||
adminCheckResult = m.policySimulatorAdminCheck(principal, adminActionNames, resource, dataReceiver)
|
||||
if !adminCheckResult {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -299,10 +305,9 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
|
||||
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(
|
||||
ListRoles, err := m.IAMListRolesClient.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationControl,
|
||||
@@ -317,8 +322,16 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
|
||||
for _, role := range ListRoles.Roles {
|
||||
//name := user.UserName
|
||||
principal := role.Arn
|
||||
adminCheckResult = m.policySimulatorAdminCheck(principal, adminActionNames, resource, dataReceiver)
|
||||
if !adminCheckResult {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -344,7 +357,7 @@ func (m *IamSimulatorModule) getPolicySimulatorResult(principal *string, actionN
|
||||
//var resourceArns = []string{"*"}
|
||||
|
||||
for {
|
||||
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
|
||||
SimulatePrincipalPolicy, err := m.IAMSimulatePrincipalPolicyClient.SimulatePrincipalPolicy(
|
||||
context.TODO(),
|
||||
&iam.SimulatePrincipalPolicyInput{
|
||||
Marker: PaginationControl2,
|
||||
@@ -386,25 +399,34 @@ func (m *IamSimulatorModule) getPolicySimulatorResult(principal *string, actionN
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
SimulatePrincipalPolicy, err := m.IAMSimulatePrincipalPolicyClient.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 +450,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
|
||||
}
|
||||
+161
-67
@@ -10,30 +10,41 @@ import (
|
||||
"strings"
|
||||
"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/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
|
||||
UserDataAttributesOnly bool
|
||||
AWSProfile string
|
||||
EC2Client *ec2.Client
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
IAMListInstanceProfilesClient iam.ListInstanceProfilesAPIClient
|
||||
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
|
||||
}
|
||||
|
||||
@@ -46,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) {
|
||||
@@ -54,48 +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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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.IAMSimulatePrincipalPolicyClient, 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
|
||||
|
||||
@@ -142,7 +174,6 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
|
||||
|
||||
m.output.CallingModule = "instance-userdata"
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -150,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)
|
||||
@@ -166,11 +197,12 @@ 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(m.MappedInstances) > 0 {
|
||||
// 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 {
|
||||
fmt.Printf("%s", userDataOut)
|
||||
}
|
||||
@@ -179,7 +211,9 @@ 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][%s] No user data found, skipping the creation of an output file\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,12 +223,15 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi
|
||||
m.output.Headers = []string{
|
||||
//"ID",
|
||||
"Name",
|
||||
"Arn",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Profile",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
//Table rows
|
||||
for _, instance := range m.MappedInstances {
|
||||
@@ -204,30 +241,34 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi
|
||||
[]string{
|
||||
//instance.ID,
|
||||
instance.Name,
|
||||
instance.Arn,
|
||||
//instance.Arn,
|
||||
instance.ID,
|
||||
instance.AvailabilityZone,
|
||||
instance.State,
|
||||
instance.ExternalIP,
|
||||
instance.PrivateIP,
|
||||
instance.Profile,
|
||||
instance.Role,
|
||||
instance.Admin,
|
||||
instance.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
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)
|
||||
//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(outputDirectory)
|
||||
fmt.Printf("[%s] %s instances found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
m.writeLoot(m.output.FilePath)
|
||||
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) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -259,19 +300,28 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
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: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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) {
|
||||
@@ -328,7 +378,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)
|
||||
}
|
||||
}
|
||||
@@ -350,6 +400,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 {
|
||||
@@ -359,15 +411,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"
|
||||
@@ -375,16 +419,66 @@ 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),
|
||||
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.IAMListInstanceProfilesClient.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+472
-71
@@ -8,19 +8,20 @@ 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/apigateway"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apprunner"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudfront"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"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/eks"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/glue"
|
||||
"github.com/aws/aws-sdk-go-v2/service/grafana"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
@@ -31,8 +32,11 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/redshift"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -59,15 +63,21 @@ type Inventory2Module struct {
|
||||
CloudfrontClient *cloudfront.Client
|
||||
AppRunnerClient *apprunner.Client
|
||||
LightsailClient *lightsail.Client
|
||||
GlueClient *glue.Client
|
||||
SNSClient *sns.Client
|
||||
SQSClient *sqs.Client
|
||||
DynamoDBClient *dynamodb.Client
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
RegionResourceCount int
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
GlobalResourceCounts []GlobalResourceCount2
|
||||
serviceMap map[string]map[string]int
|
||||
services []string
|
||||
@@ -75,8 +85,8 @@ type Inventory2Module struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
globalOutput utils.OutputData2
|
||||
output internal.OutputData2
|
||||
globalOutput internal.OutputData2
|
||||
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
@@ -92,45 +102,48 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "inventory"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": "inventory",
|
||||
},
|
||||
)
|
||||
// def change this to build dynamically in the future.
|
||||
m.services = []string{"total", "APIGateway RestAPIs", "APIGatewayv2 APIs", "AppRunner Services", "CloudFormation Stacks", "Cloudfront Distributions", "EC2 Instances", "ECS Tasks", "EKS Clusters", "ELB Load Balancers", "ELBv2 Load Balancers", "Grafana Workspaces", "Lambda Functions", "Lightsail Instances/Containers", "MQ Brokers", "OpenSearch DomainNames", "RDS DB Instances", "SecretsManager Secrets", "SSM Parameters"}
|
||||
m.services = []string{"total", "APIGateway RestAPIs", "APIGatewayv2 APIs", "AppRunner Services", "CloudFormation Stacks", "Cloudfront Distributions", "DynamoDB Tables", "EC2 Instances", "ECS Tasks", "EKS Clusters", "ELB Load Balancers", "ELBv2 Load Balancers", "Glue Dev Endpoints", "Glue Jobs", "Grafana Workspaces", "Lambda Functions", "Lightsail Instances/Containers", "MQ Brokers", "OpenSearch DomainNames", "RDS DB Instances", "SecretsManager Secrets", "SNS Topics", "SQS Queues", "SSM Parameters"}
|
||||
m.serviceMap = map[string]map[string]int{}
|
||||
m.totalRegionCounts = map[string]int{}
|
||||
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
//initialize servicemap and total
|
||||
for _, service := range m.services {
|
||||
m.serviceMap[service] = map[string]int{}
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
m.serviceMap[service][region] = 0
|
||||
m.totalRegionCounts[region] = 0
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating selected services in all regions for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s] Supported Services: ApiGateway, ApiGatewayv2, AppRunner, CloudFormation, Cloudfront, EC2, ECS, EKS, \n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s] \t\t\tELB, ELBv2, Grafana, IAM, Lambda, Lightsail, MQ, OpenSearch, RDS, S3, SecretsManager, SSM\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] Enumerating selected services 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: ApiGateway, ApiGatewayv2, AppRunner, CloudFormation, Cloudfront, DynamoDB, \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
fmt.Printf("[%s][%s] \t\t\tEC2, ECS, EKS, ELB, ELBv2, Glue, Grafana, IAM, Lambda, Lightsail, MQ, \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
fmt.Printf("[%s][%s] \t\t\tOpenSearch, RDS, S3, SecretsManager, SNS, SQS, SSM\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, 50)
|
||||
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 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 GlobalResourceCount2)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
@@ -149,6 +162,8 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi
|
||||
// }
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
@@ -244,14 +259,14 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
//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.PrintGlobalResources(outputFormat, outputDirectory, verbosity, dataReceiver)
|
||||
m.PrintTotalResources(outputFormat)
|
||||
} else {
|
||||
fmt.Printf("[%s] No resources identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No resources identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
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)
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
}
|
||||
@@ -269,8 +284,6 @@ func (m *Inventory2Module) PrintGlobalResources(outputFormat string, outputDirec
|
||||
//m.globalOutput.CallingModule = fmt.Sprintf("%s-global", m.globalOutput.CallingModule)
|
||||
m.globalOutput.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
|
||||
|
||||
|
||||
m.globalOutput.Headers = []string{
|
||||
"Resource Type",
|
||||
"Total",
|
||||
@@ -289,11 +302,12 @@ func (m *Inventory2Module) PrintGlobalResources(outputFormat string, outputDirec
|
||||
}
|
||||
//m.globalOutput.FilePath = filepath.Join(path, m.globalOutput.CallingModule)
|
||||
//m.globalOutput.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.globalOutput.Headers, m.globalOutput.Body, m.globalOutput.FilePath, m.globalOutput.FullFilename, m.globalOutput.CallingModule)
|
||||
internal.OutputSelector(verbosity, outputFormat, m.globalOutput.Headers, m.globalOutput.Body, m.globalOutput.FilePath, m.globalOutput.FullFilename, m.globalOutput.CallingModule, false, m.AWSProfile)
|
||||
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) Receiver(receiver chan GlobalResourceCount2, receiverDone chan bool) {
|
||||
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
@@ -309,74 +323,209 @@ func (m *Inventory2Module) Receiver(receiver chan GlobalResourceCount2, receiver
|
||||
func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan GlobalResourceCount2) {
|
||||
defer wg.Done()
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLambdaFunctionsPerRegion(r, wg, semaphore)
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getEc2InstancesPerRegion(r, wg, semaphore)
|
||||
res, err := servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLambdaFunctionsPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getCloudFormationStacksPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("ec2", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getEc2InstancesPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getSecretsManagerSecretsPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("cloudformation", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getCloudFormationStacksPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getEksClustersPerRegion(r, wg, semaphore)
|
||||
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)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getEcsTasksPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("eks", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getEksClustersPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getRdsClustersPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("ecs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getEcsTasksPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayvAPIsPerRegion(r, wg, semaphore)
|
||||
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)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("apigateway", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayvAPIsPerRegion(r, wg, semaphore)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getELBv2ListenersPerRegion(r, wg, semaphore)
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getELBListenersPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("elb", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getELBv2ListenersPerRegion(r, wg, semaphore)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getMqBrokersPerRegion(r, wg, semaphore)
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getELBListenersPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getOpenSearchPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("mq", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getMqBrokersPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getGrafanaWorkspacesPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("es", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getOpenSearchPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("grafana", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getGrafanaWorkspacesPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
// 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.getAppRunnerServicesPerRegion(r, wg, semaphore)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLightsailInstancesAndContainersPerRegion(r, wg, semaphore)
|
||||
res, err = servicemap.IsServiceInRegion("lightsail", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLightsailInstancesAndContainersPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getSSMParametersPerRegion(r, wg, semaphore)
|
||||
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)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("glue", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getGlueDevEndpointsPerRegion(r, wg, semaphore)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("ssm", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getGlueJobsPerRegion(r, wg, semaphore)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("sns", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getSNSTopicsPerRegion(r, wg, semaphore)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("sqs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getSQSQueuesPerRegion(r, wg, semaphore)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("dynamodb", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getDynamoDBTablesPerRegion(r, wg, semaphore)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
@@ -1017,7 +1166,6 @@ func (m *Inventory2Module) getOpenSearchPerRegion(r string, wg *sync.WaitGroup,
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
m.mu.Unlock()
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1321,6 +1469,259 @@ func (m *Inventory2Module) getEcsTasksPerRegion(r string, wg *sync.WaitGroup, se
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) getGlueDevEndpointsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
// Don't use this method as a template for future ones. There is a one off in the way the NextToken is handled.
|
||||
defer func() {
|
||||
wg.Done()
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "Glue Dev Endpoints"
|
||||
var PaginationControl *string
|
||||
|
||||
for {
|
||||
ListDevEndpoints, err := m.GlueClient.ListDevEndpoints(
|
||||
context.TODO(),
|
||||
&(glue.ListDevEndpointsInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *glue.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListDevEndpoints.DevEndpointNames)
|
||||
|
||||
// This next line is non-standard. For some reason this next token is an empty string instead of nil, so
|
||||
// as a result we had to change the comparison.
|
||||
if aws.ToString(ListDevEndpoints.NextToken) != "" {
|
||||
PaginationControl = ListDevEndpoints.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
m.mu.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) getGlueJobsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "Glue Jobs"
|
||||
|
||||
for {
|
||||
m.modLog.Info(fmt.Sprintf("Getting jobs %v\n", PaginationControl))
|
||||
ListJobs, err := m.GlueClient.ListJobs(
|
||||
context.TODO(),
|
||||
&(glue.ListJobsInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *glue.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListJobs.JobNames)
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListJobs.NextToken != nil {
|
||||
PaginationControl = ListJobs.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
m.mu.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "SNS Topics"
|
||||
|
||||
for {
|
||||
ListTopics, err := m.SNSClient.ListTopics(
|
||||
context.TODO(),
|
||||
&(sns.ListTopicsInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *sns.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListTopics.Topics)
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListTopics.NextToken != nil {
|
||||
PaginationControl = ListTopics.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
m.mu.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "SQS Queues"
|
||||
|
||||
for {
|
||||
ListQueues, err := m.SQSClient.ListQueues(
|
||||
context.TODO(),
|
||||
&(sqs.ListQueuesInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *sqs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListQueues.QueueUrls)
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListQueues.NextToken != nil {
|
||||
PaginationControl = ListQueues.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
m.mu.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "DynamoDB Tables"
|
||||
|
||||
ListTables, err := m.DynamoDBClient.ListTables(
|
||||
context.TODO(),
|
||||
&(dynamodb.ListTablesInput{}),
|
||||
func(o *dynamodb.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListTables.TableNames)
|
||||
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Global Resources
|
||||
|
||||
func (m *Inventory2Module) getBuckets(verbosity int, dataReceiver chan GlobalResourceCount2) {
|
||||
@@ -1468,8 +1869,8 @@ func (m *Inventory2Module) PrintTotalResources(outputFormat string) {
|
||||
}
|
||||
}
|
||||
|
||||
for i, _ := range m.GlobalResourceCounts {
|
||||
for i := range m.GlobalResourceCounts {
|
||||
totalResources = totalResources + m.GlobalResourceCounts[i].count
|
||||
}
|
||||
fmt.Printf("[%s] %d resources enumerated in the services we looked at. This is NOT the total number of resources in the account.\n", cyan(m.output.CallingModule), totalResources)
|
||||
fmt.Printf("[%s][%s] %d resources found in the services we looked at. This is NOT the total number of resources in the account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), totalResources)
|
||||
}
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"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/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
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
|
||||
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.IAMSimulatePrincipalPolicyClient, 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.
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
//"Type",
|
||||
"Resource Arn",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
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", m.AWSProfile)
|
||||
//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] %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: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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 {
|
||||
m.modLog.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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -25,14 +25,16 @@ type OutboundAssumedRolesModule struct {
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
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
|
||||
}
|
||||
@@ -117,36 +119,37 @@ 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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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)
|
||||
|
||||
// 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 {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, dataReceiver)
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
@@ -154,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
|
||||
|
||||
@@ -190,14 +192,15 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
|
||||
|
||||
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)
|
||||
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)
|
||||
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) {
|
||||
@@ -213,17 +216,34 @@ func (m *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEn
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
defer wg.Done()
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getAssumeRoleLogEntriesPerRegion(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
//var LookupAttributes []types.LookupAttributes
|
||||
@@ -240,7 +260,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
|
||||
EndTime: endTime,
|
||||
StartTime: &startTime,
|
||||
LookupAttributes: []cloudtrailTypes.LookupAttribute{
|
||||
cloudtrailTypes.LookupAttribute{
|
||||
{
|
||||
AttributeKey: cloudtrailTypes.LookupAttributeKeyEventName,
|
||||
AttributeValue: aws.String("AssumeRole"),
|
||||
},
|
||||
|
||||
+13
-12
@@ -10,8 +10,7 @@ import (
|
||||
"strconv"
|
||||
"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/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
@@ -26,7 +25,9 @@ type IamPermissionsModule struct {
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
|
||||
@@ -35,9 +36,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
|
||||
}
|
||||
|
||||
@@ -87,14 +88,14 @@ 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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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))
|
||||
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)))
|
||||
@@ -137,14 +138,14 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//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)
|
||||
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(principal string) {
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
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 = graph.New(graph.StringHash, graph.Directed())
|
||||
|
||||
for _, node := range m.Nodes {
|
||||
_ = m.pmapperGraph.AddVertex(node.Arn)
|
||||
|
||||
}
|
||||
|
||||
for _, edge := range m.Edges {
|
||||
_ = m.pmapperGraph.AddEdge(edge.Source, edge.Destination)
|
||||
}
|
||||
|
||||
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) 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", m.AWSProfile)
|
||||
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, this cloudfox command and others will attempt to find and use the pmapper graph data in this and other cloudfox commands\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
|
||||
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", m.AWSProfile)
|
||||
//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)
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
+16
-13
@@ -5,10 +5,8 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"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/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
@@ -22,15 +20,17 @@ type IamPrincipalsModule struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -67,19 +67,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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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)
|
||||
@@ -137,15 +137,17 @@ 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.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)
|
||||
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++
|
||||
@@ -156,6 +158,7 @@ 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.
|
||||
|
||||
+120
-58
@@ -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"
|
||||
)
|
||||
|
||||
@@ -22,13 +23,15 @@ type RAMModule struct {
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
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
|
||||
}
|
||||
|
||||
@@ -37,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) {
|
||||
@@ -44,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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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 {
|
||||
@@ -75,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
|
||||
|
||||
@@ -86,11 +93,10 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"URI",
|
||||
"PushedAt",
|
||||
"ImageTags",
|
||||
"ImageSize",
|
||||
"Share Name",
|
||||
"Type",
|
||||
"Owner",
|
||||
"Share Type",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
@@ -101,7 +107,9 @@ 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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -109,22 +117,32 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
|
||||
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)
|
||||
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)
|
||||
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: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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) {
|
||||
@@ -143,44 +161,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+214
-49
@@ -7,11 +7,12 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
@@ -21,35 +22,108 @@ import (
|
||||
|
||||
type RoleTrustsModule struct {
|
||||
// General configuration data
|
||||
IAMClient iam.ListRolesAPIClient
|
||||
IAMClientListRoles iam.ListRolesAPIClient
|
||||
IAMClient *iam.Client
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSProfile string
|
||||
CommandCounter console.CommandCounter
|
||||
Goroutines int
|
||||
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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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.IAMSimulatePrincipalPolicyClient, 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()
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
m.printPrincipalTrusts(outputFormat, outputDirectory)
|
||||
m.printServiceTrusts(outputFormat, outputDirectory)
|
||||
m.printFederatedTrusts(outputFormat, outputDirectory)
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) {
|
||||
@@ -61,15 +135,19 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirec
|
||||
"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 {
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, principal := range statement.Principal.AWS {
|
||||
column1 := 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
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,12 +155,14 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirec
|
||||
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)))
|
||||
//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))
|
||||
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) {
|
||||
@@ -94,15 +174,19 @@ func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirecto
|
||||
"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 {
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, service := range statement.Principal.Service {
|
||||
column1 := 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
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,14 +195,98 @@ func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirecto
|
||||
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)))
|
||||
//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))
|
||||
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) {
|
||||
m.output.FullFilename = ""
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-federated"
|
||||
var column3, column2 string
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Provider",
|
||||
"Trusted Subject",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
column1 := 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
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
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() {
|
||||
sort.Slice(
|
||||
m.output.Body,
|
||||
@@ -132,9 +300,9 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
|
||||
// "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).
|
||||
// This for loop exits at the end depending on whether the output hits its last page (see pagination control block at the end of the loop).
|
||||
for {
|
||||
results, err := m.IAMClient.ListRoles(
|
||||
results, err := m.IAMClientListRoles.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationMarker,
|
||||
@@ -153,7 +321,16 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
|
||||
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.
|
||||
@@ -166,32 +343,20 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+21
-22
@@ -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
|
||||
@@ -25,13 +21,15 @@ type Route53Module struct {
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
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
|
||||
}
|
||||
@@ -50,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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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()
|
||||
|
||||
@@ -87,18 +85,19 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string
|
||||
|
||||
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(outputDirectory, 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)
|
||||
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) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -131,13 +130,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 {
|
||||
@@ -149,13 +148,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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
+79
-30
@@ -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"
|
||||
)
|
||||
|
||||
@@ -25,13 +25,15 @@ type SecretsModule struct {
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
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
|
||||
}
|
||||
@@ -48,41 +50,44 @@ 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 = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
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)
|
||||
|
||||
// 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, "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 {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, dataReceiver)
|
||||
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
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
@@ -112,14 +117,16 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string
|
||||
|
||||
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)
|
||||
m.writeLoot(outputDirectory, 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)
|
||||
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) {
|
||||
@@ -135,19 +142,35 @@ func (m *SecretsModule) Receiver(receiver chan Secret, receiverDone chan bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SecretsModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Secret) {
|
||||
func (m *SecretsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Secret) {
|
||||
defer wg.Done()
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getSecretsManagerSecretsPerRegion(r, dataReceiver)
|
||||
m.getSSMParametersPerRegion(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *SecretsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -179,17 +202,30 @@ 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)
|
||||
|
||||
}
|
||||
|
||||
func (m *SecretsModule) getSecretsManagerSecretsPerRegion(r string, dataReceiver chan Secret) {
|
||||
func (m *SecretsModule) getSecretsManagerSecretsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Secret) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
for {
|
||||
@@ -234,7 +270,20 @@ func (m *SecretsModule) getSecretsManagerSecretsPerRegion(r string, dataReceiver
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SecretsModule) getSSMParametersPerRegion(r string, dataReceiver chan Secret) {
|
||||
func (m *SecretsModule) getSSMParametersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Secret) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"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/fatih/color"
|
||||
)
|
||||
|
||||
var cyan = color.New(color.FgCyan).SprintFunc()
|
||||
var red = color.New(color.FgRed).SprintFunc()
|
||||
|
||||
func initIAMSimClient(iamSimPPClient iam.SimulatePrincipalPolicyAPIClient, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
|
||||
|
||||
iamSimMod := IamSimulatorModule{
|
||||
IAMSimulatePrincipalPolicyClient: iamSimPPClient,
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
}
|
||||
|
||||
return iamSimMod
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
edgesPath = fmt.Sprintf("~/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/edges.json")
|
||||
nodesPath = fmt.Sprintf("~/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/nodes.json")
|
||||
}
|
||||
|
||||
} 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
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
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/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type TagsModule struct {
|
||||
// General configuration data
|
||||
ResourceGroupsTaggingApiClient *resourcegroupstaggingapi.Client
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// 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", m.AWSProfile)
|
||||
//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] %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)
|
||||
} 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.
|
||||
var PaginationControl *string
|
||||
|
||||
for {
|
||||
GetResources, err := m.ResourceGroupsTaggingApiClient.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++
|
||||
break
|
||||
}
|
||||
//var parsedArn types.Arn
|
||||
for _, resource := range GetResources.ResourceTagMappingList {
|
||||
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,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if aws.ToString(GetResources.PaginationToken) != "" {
|
||||
PaginationControl = GetResources.PaginationToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage"
|
||||
"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 AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, AzVerbosity int, AzWrapTable bool) error {
|
||||
var err error
|
||||
var header []string
|
||||
var body [][]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, 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)
|
||||
header, body, err = getStorageInfoPerSubscription(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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, error) {
|
||||
var err error
|
||||
var header []string
|
||||
var body, b [][]string
|
||||
|
||||
for _, s := range getSubscriptionsPerTenantID(AzTenantID) {
|
||||
header, b, err = getRelevantStorageAccountData(ptr.ToString(s.SubscriptionID))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else {
|
||||
body = append(body, b...)
|
||||
}
|
||||
}
|
||||
return header, body, nil
|
||||
}
|
||||
|
||||
func getStorageInfoPerSubscription(AzSubscriptionID string) ([]string, [][]string, error) {
|
||||
var err error
|
||||
var header []string
|
||||
var body [][]string
|
||||
|
||||
for _, s := range getSubscriptions() {
|
||||
if ptr.ToString(s.SubscriptionID) == AzSubscriptionID {
|
||||
header, body, err = getRelevantStorageAccountData(ptr.ToString(s.SubscriptionID))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return header, body, nil
|
||||
}
|
||||
|
||||
func getRelevantStorageAccountData(subscriptionID string) ([]string, [][]string, error) {
|
||||
tableHeader := []string{"Subscription ID", "Storage Account Name", "Kind", "Public Blob Allowed"}
|
||||
var tableBody [][]string
|
||||
storageAccounts, err := getStorageAccounts(subscriptionID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, sa := range storageAccounts {
|
||||
tableBody = append(tableBody,
|
||||
[]string{
|
||||
subscriptionID,
|
||||
ptr.ToString(sa.Name),
|
||||
string(sa.Kind),
|
||||
strconv.FormatBool(ptr.ToBool(sa.AllowBlobPublicAccess)),
|
||||
},
|
||||
)
|
||||
}
|
||||
return tableHeader, tableBody, 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
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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": [
|
||||
"/"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
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 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"`
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
)
|
||||
|
||||
func TestAzWhoamiCommand(t *testing.T) {
|
||||
fmt.Println()
|
||||
fmt.Println("[test case] Azure Whoami Command")
|
||||
|
||||
// Mocked functions to simulate Azure calls and responses
|
||||
getTenants = mockedGetTenants
|
||||
getSubscriptions = mockedGetSubscriptions
|
||||
getResourceGroups = mockedGetResourceGroups
|
||||
|
||||
// Test case parameters
|
||||
internal.MockFileSystem(true)
|
||||
subtests := []struct {
|
||||
name string
|
||||
resourcesTestFile string
|
||||
azExtendedFilter bool
|
||||
version string
|
||||
wrapTableOutput bool
|
||||
}{
|
||||
{
|
||||
name: "./cloudfox azure whoami",
|
||||
resourcesTestFile: "./test-data/resources.json",
|
||||
azExtendedFilter: false,
|
||||
version: "DEV",
|
||||
wrapTableOutput: false,
|
||||
},
|
||||
{
|
||||
name: "./cloudfox azure whoami --extended",
|
||||
resourcesTestFile: "./test-data/resources.json",
|
||||
azExtendedFilter: true,
|
||||
version: "DEV",
|
||||
wrapTableOutput: true,
|
||||
},
|
||||
}
|
||||
for _, s := range subtests {
|
||||
globals.RESOURCES_TEST_FILE = s.resourcesTestFile
|
||||
fmt.Println()
|
||||
fmt.Printf("[subtest] %s\n", s.name)
|
||||
AzWhoamiCommand(s.version, s.wrapTableOutput)
|
||||
}
|
||||
}
|
||||
+1109
-549
File diff suppressed because it is too large.
Load diff
+105
-37
@@ -1,67 +1,135 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/BishopFox/cloudfox/azure"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
AzTenantID string
|
||||
AzSubscriptionID string
|
||||
AzRGName string
|
||||
AzOutputFormat string
|
||||
AzOutputDirectory string
|
||||
AzVerbosity int
|
||||
AzCommands = &cobra.Command{
|
||||
AzWrapTable bool
|
||||
|
||||
AzCommands = &cobra.Command{
|
||||
Use: "azure",
|
||||
Aliases: []string{"az"},
|
||||
Long: `
|
||||
See \"Available Commands\" for Azure Modules`,
|
||||
Short: "See \"Available Commands\" for Azure Modules",
|
||||
|
||||
Long: `See "Available Commands" for Azure Modules below`,
|
||||
Short: "See \"Available Commands\" for Azure Modules below",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
AzInstancesMapRGFilter string
|
||||
AzInstancesMapCommand = &cobra.Command{
|
||||
Use: "instances",
|
||||
Aliases: []string{"instances-map"},
|
||||
Short: `Enumerates compute instances for specified Resource Group`,
|
||||
AzWhoamiCommand = &cobra.Command{
|
||||
Use: "whoami",
|
||||
Aliases: []string{},
|
||||
Short: "Display available Azure CLI sessions",
|
||||
Long: `
|
||||
Enumerates compute instances for specified Resource Group`,
|
||||
Display Available Azure CLI Sessions:
|
||||
./cloudfox az whoami`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
color.Red("This subcommand is under development! Use at your own risk!")
|
||||
m := azure.InstancesMapModule{Scope: utils.AzGetScopeInformation()}
|
||||
m.InstancesMap(AzVerbosity, AzOutputFormat, AzOutputDirectory, AzInstancesMapRGFilter)
|
||||
azure.AzWhoamiCommand(cmd.Root().Version, AzWrapTable)
|
||||
},
|
||||
}
|
||||
AzUserFilter string
|
||||
AzRBACMapCommand = &cobra.Command{
|
||||
Use: "rbac-map",
|
||||
Aliases: []string{"rbac"},
|
||||
Short: "Display all role assignemts for all principals",
|
||||
AzRBACCommand = &cobra.Command{
|
||||
Use: "rbac",
|
||||
Aliases: []string{},
|
||||
Short: "Display role assignemts for Azure principals",
|
||||
Long: `
|
||||
Display all role assignemts for all principals`,
|
||||
Enumerate role assignments for a specific tenant:
|
||||
./cloudfox az rbac --tenant TENANT_ID
|
||||
|
||||
Enumerate role assignments for a specific subscription:
|
||||
./cloudfox az rbac --subscription SUBSCRIPTION_ID
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
color.Red("This subcommand is under development! Use at your own risk!")
|
||||
m := azure.RBACMapModule{Scope: utils.AzGetScopeInformation()}
|
||||
m.RBACMap(AzVerbosity, AzOutputFormat, AzOutputDirectory, AzUserFilter)
|
||||
err := azure.AzRBACCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
AzInstancesCommand = &cobra.Command{
|
||||
Use: "instances",
|
||||
Aliases: []string{},
|
||||
Short: "Enumerates Azure Compute instances",
|
||||
Long: `
|
||||
Enumerate VMs for a specific tenant:
|
||||
./cloudfox az instances --tenant TENANT_ID
|
||||
|
||||
Enumerate VMs for a specific subscription:
|
||||
./cloudfox az instances --subscription SUBSCRIPTION_ID`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := azure.AzInstancesCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
AzStorageCommand = &cobra.Command{
|
||||
Use: "storage",
|
||||
Aliases: []string{},
|
||||
Short: "Enumerates azure storage accounts",
|
||||
Long: `
|
||||
Enumerate storage accounts for a specific tenant:
|
||||
./cloudfox az storage --tenant TENANT_ID
|
||||
|
||||
Enumerate storage accounts for a specific subscription:
|
||||
./cloudfox az storage --subscription SUBSCRIPTION_ID
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
azure.AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Global flags for the Azure modules
|
||||
AzCommands.PersistentFlags().StringVarP(&AzOutputFormat, "output", "o", "all", "[\"table\" | \"csv\" | \"all\" ]")
|
||||
AzCommands.PersistentFlags().IntVarP(&AzVerbosity, "verbosity", "v", 1, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n")
|
||||
AzCommands.PersistentFlags().StringVar(&AzOutputDirectory, "outdir", "cloudfox-output", "Output Directory ")
|
||||
// Global flags
|
||||
AzCommands.PersistentFlags().StringVarP(
|
||||
&AzOutputFormat,
|
||||
"output",
|
||||
"o",
|
||||
"all",
|
||||
"[\"table\" | \"csv\" | \"all\" ]")
|
||||
AzCommands.PersistentFlags().IntVarP(
|
||||
&AzVerbosity,
|
||||
"verbosity",
|
||||
"v",
|
||||
2,
|
||||
"1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n")
|
||||
AzCommands.PersistentFlags().StringVarP(
|
||||
&AzTenantID,
|
||||
"tenant",
|
||||
"t",
|
||||
"",
|
||||
"Tenant name")
|
||||
AzCommands.PersistentFlags().StringVarP(
|
||||
&AzSubscriptionID,
|
||||
"subscription",
|
||||
"s",
|
||||
"",
|
||||
"Subscription name")
|
||||
AzCommands.PersistentFlags().StringVarP(
|
||||
&AzRGName,
|
||||
"resource-group",
|
||||
"g",
|
||||
"",
|
||||
"Resource Group name")
|
||||
AzCommands.PersistentFlags().BoolVarP(
|
||||
&AzWrapTable,
|
||||
"wrap",
|
||||
"w",
|
||||
false,
|
||||
"Wrap table to fit in terminal (complicates grepping)")
|
||||
|
||||
// Instances Map Module Flags
|
||||
AzInstancesMapCommand.Flags().StringVarP(&AzInstancesMapRGFilter, "resource-group", "g", "all", "Name of Resource Group to query")
|
||||
|
||||
// RBAC Map Module Flags
|
||||
AzRBACMapCommand.Flags().StringVarP(&AzUserFilter, "user", "u", "all", "Display name of user to query")
|
||||
|
||||
AzCommands.AddCommand(AzInstancesMapCommand, AzRBACMapCommand)
|
||||
AzCommands.AddCommand(
|
||||
AzWhoamiCommand,
|
||||
AzRBACCommand,
|
||||
AzInstancesCommand,
|
||||
AzStorageCommand)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
const clearln = "\r\x1b[2K"
|
||||
|
||||
var (
|
||||
cyan = color.New(color.FgCyan).SprintFunc()
|
||||
green = color.New(color.FgGreen).SprintFunc()
|
||||
)
|
||||
|
||||
type CommandCounter struct {
|
||||
Total int
|
||||
Pending int
|
||||
Complete int
|
||||
Error int
|
||||
Executing int
|
||||
}
|
||||
|
||||
func SpinUntil(callingModuleName string, counter *CommandCounter, done chan bool, spinType string) {
|
||||
defer close(done)
|
||||
for {
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
fmt.Printf(clearln+"[%s] Status: %d/%d %s complete (%d errors -- For details check %s)", cyan(callingModuleName), counter.Complete, counter.Total, spinType, counter.Error, fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(utils.GetLogDirPath())))
|
||||
case <-done:
|
||||
fmt.Printf(clearln+"[%s] Status: %d/%d %s complete (%d errors -- For details check %s)\n", cyan(callingModuleName), counter.Complete, counter.Complete, spinType, counter.Error, fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(utils.GetLogDirPath())))
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package globals
|
||||
|
||||
// Module directory tree
|
||||
const AZ_DIR_BASE = "azure"
|
||||
const AZ_DIR_TEN = "tenants"
|
||||
const AZ_DIR_SUB = "subscriptions"
|
||||
|
||||
// Test file full names and paths
|
||||
var STORAGE_ACCOUNTS_TEST_FILE string
|
||||
var VMS_TEST_FILE string
|
||||
var NICS_TEST_FILE string
|
||||
var PUBLIC_IPS_TEST_FILE string
|
||||
var RESOURCES_TEST_FILE string
|
||||
var ROLE_DEFINITIONS_TEST_FILE string
|
||||
var ROLE_ASSIGNMENTS_TEST_FILE string
|
||||
var AAD_USERS_TEST_FILE string
|
||||
|
||||
// Module names
|
||||
const AZ_WHOAMI_MODULE_NAME = "whoami"
|
||||
const AZ_INTERACTIVE_MENU_MODULE_NAME = "interactive-menu"
|
||||
const AZ_INSTANCES_MODULE_NAME = "instances"
|
||||
const AZ_RBAC_MODULE_NAME = "rbac"
|
||||
const AZ_STORAGE_MODULE_NAME = "storage"
|
||||
|
||||
// Microsoft endpoints
|
||||
const AZ_RESOURCE_MANAGER_ENDPOINT = "https://management.azure.com/"
|
||||
const AZ_GRAPH_ENDPOINT = "https://graph.windows.net/"
|
||||
@@ -0,0 +1,5 @@
|
||||
package globals
|
||||
|
||||
const CLOUDFOX_USER_AGENT = "cloudfox"
|
||||
const CLOUDFOX_LOG_FILE_DIR_NAME = ".cloudfox"
|
||||
const CLOUDFOX_BASE_DIRECTORY = "cloudfox-output"
|
||||
@@ -1,55 +1,101 @@
|
||||
module github.com/BishopFox/cloudfox
|
||||
|
||||
go 1.16
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go v60.2.0+incompatible
|
||||
github.com/Azure/go-autorest/autorest v0.11.23
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.10
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.4 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v67.1.0+incompatible
|
||||
github.com/Azure/go-autorest/autorest v0.11.28
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
|
||||
github.com/aquasecurity/table v1.8.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.11
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.0
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.6
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.5
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.12.4
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.18.0
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.18.2
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.16.1
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.26.0
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.5
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.18.7
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.17.4
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.18.0
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.5
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.5
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.24.1
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.9.1
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.5
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.23.1
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.22.1
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.1
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.9.5
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.15
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.16.0
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.25.0
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.20.5
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.24.0
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.32.1
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.13.0
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.1
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0
|
||||
github.com/aws/smithy-go v1.12.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.25
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.23
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.15.0
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.24.1
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.21.0
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.20.3
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.7
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.72.1
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.23
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.20.0
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.18.0
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.23.0
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.23
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.26
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.25.4
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.35.0
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.10.0
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.23
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.25.1
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.24.0
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.15
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.11.3
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.24
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.31.0
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.26.16
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.13.24
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.25.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.55.0
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5
|
||||
github.com/aws/smithy-go v1.13.4
|
||||
github.com/bishopfox/awsservicemap v1.0.0
|
||||
github.com/dominikbraun/graph v0.15.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/go-openapi/strfmt v0.21.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/spf13/cobra v1.4.0
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/afero v1.9.3
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/crypto v0.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/go-openapi/errors v0.20.3 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
go.mongodb.org/mongo-driver v1.11.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/term v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
@@ -36,27 +36,26 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go v60.2.0+incompatible h1:twJOQQl3um6o+1q5SSyAdLr2GCX8dJPVRNS1eKOnv54=
|
||||
github.com/Azure/azure-sdk-for-go v60.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go v67.1.0+incompatible h1:oziYcaopbnIKfM69DL05wXdypiqfrUKdxUKrKpynJTw=
|
||||
github.com/Azure/azure-sdk-for-go v67.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
|
||||
github.com/Azure/go-autorest/autorest v0.11.23 h1:bRQWsW25/YkoxnIqXMPF94JW33qWDcrPMZ3bINaAruU=
|
||||
github.com/Azure/go-autorest/autorest v0.11.23/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ=
|
||||
github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
|
||||
github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
|
||||
github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.10 h1:F9A3Z++TtAoFysBsNOIJILoHuYBaYvhVGsMGEqPtIS8=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.10/go.mod h1:zQXYYNX9kXzRMrJNVXWUfNy38oPMF5/2TeZ4Wylc9fE=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.4 h1:iuooz5cZL6VRcO7DVSFYxRcouqn6bFVE/e77Wts50Zk=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.4/go.mod h1:yAQ2b6eP/CmLPnmLvxtT1ALIY3OR1oFcCqVBi8vHiTc=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac=
|
||||
@@ -69,109 +68,113 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/aquasecurity/table v1.8.0 h1:9ntpSwrUfjrM6/YviArlx/ZBGd6ix8W+MtojQcM7tv0=
|
||||
github.com/aquasecurity/table v1.8.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.4/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.5/go.mod h1:Wh7MEsmEApyL5hrWzpDkba4gwAPc5/piwLVLFnCxp48=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.11 h1:xM1ZPSvty3xVmdxiGr7ay/wlqv+MWhH0rMlyLdbC0YQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.11/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.2.0 h1:scBthy70MB3m4LCMFaBcmYCyR2XWOz6MxSfdSu/+fQo=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.2.0/go.mod h1:oZHzg1OVbuCiRTY0oRPM+c2HQvwnFCGJwKeSqqAJ/yM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.0 h1:1ij3YPk13RrIn1h+pH+dArh3lNPD5JSAP+ifOkNhnB0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.0/go.mod h1:Pjv2OafecIn+4miw9VFDCr06YhKyf/oKOkIcpQOgWKk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2/go.mod h1:SgKKNBIoDC/E1ZCDhhMW3yalWjwuLjMcpLzsM/QQnWo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11/go.mod h1:tmUB6jakq5DFNcXsXOA/ZQ7/C8VnSKYkx58OI7Fh79g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12/go.mod h1:Afj/U8svX6sJ77Q+FPWMzabJ9QjbwP32YlopgKALUpg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 h1:OmiwoVyLKEqqD5GvB683dbSqxiOfvx4U2lDZhG2Esc4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18/go.mod h1:348MLhzV1GSlZSMusdwQpXKbhD7X2gbI/TxwAPKkYZQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2/go.mod h1:xT4XX6w5Sa3dhg50JrYyy3e4WPYo/+WjY/BXtqXVunU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5/go.mod h1:fV1AaS2gFc1tM0RCb015FJ0pvWVUfJZANzjwoO4YakM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6/go.mod h1:FwpAKI+FBPIELJIdmQzlLtRe8LQSOreMcM2wBsPMvvc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 h1:5mvQDtNWtI6H56+E4LUnLWEmATMB7oEh+Z9RurtIuC0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12/go.mod h1:ckaCVTEdGAxO6KwTGzgskxR1xM+iJW4lxMyDFVda2Fc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.4 h1:0NrDHIwS1LIR750ltj6ciiu4NZLpr9rgq8vHi/4QD4s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.4/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.6 h1:GWrNZ3xtblr+J+oCYtJY67YPqR5nzObOaXWWKplDMag=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.6/go.mod h1:LAEdbmsP4QcD052UMa1RreHQiQRh3BusHyCMomn/7MQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.5 h1:6b2oPld50/hAEkjlBlxFqBQ46PGqP7DafAVPZII79j4=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.5/go.mod h1:q37rkUszPKLoS6/Mml9HbxYws6TO/dQEFaE1K21NfwA=
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.12.4 h1:u2xMm6tCbNVv7ixY69SPDhb8kmNLty+h5V77Qt6I0Ng=
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.12.4/go.mod h1:HhYO7WWN9ahpcj809PzWLW2tabbLJcptLj1dW1Z5yXs=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.18.0 h1:LovwBbWi+OKmPThc75SPHWANYmzVsl6BqRMbe3EWmws=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.18.0/go.mod h1:v8HW+9r8sQAoOpyCpPlWLU+du6saS1N+w9BIzuxTLgQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.18.2 h1:uhpcmaxRKPt/VHGmzJ4aam8sL0DYr7qJejiQzp/dBV8=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.18.2/go.mod h1:TRXCqcApTM1LhOJcLNolqFEDr3InJoYBieb1qqPcCko=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.16.1 h1:1dALZzDXZmZIgVX6N+oOSwSBH+BBWC1RCjUCvgQUQfs=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.16.1/go.mod h1:ddq0bwfwHQ0QxV6vU9arKg31qfmg8fjcYZt/ef01AcI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.26.0 h1:Q++veaxis1Dg7is9yi+aEPsIBRAgdkUxoIvyud7jOyo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.26.0/go.mod h1:cIbz+b70nxJafXf9lT07Xj03pef6CsVdYTCCR0DQEQc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.5 h1:W9vzPbvX7rOa/FacbQIDfnNrwxHkn5O+DdfmiIS4cHc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.5/go.mod h1:vk2+DbeZQFXznxJZSMnYrfnCHYxg4oT4Mdh59wSCkw4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.18.7 h1:TQuuBT5rxK8YLvGhbwIG+pVpoAuhjSG5cbitpIQgzw4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.18.7/go.mod h1:39n31o3Oxv0il3NMX161v0D6Xhzkx5HYpuh3cbGQ224=
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.17.4 h1:0gafSOSNlPETtoIbzdfDc5cyAehiuu/YvFrQKoBaxZE=
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.17.4/go.mod h1:SgJ8ZrUemmZKTF4t6drFlJVS4jKce3pi+u/JngrLm4A=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.18.0 h1:FyVLY3I21tqUjvd2ngS83F9xnNh3B3SmhZJ2Zq0DS1s=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.18.0/go.mod h1:4KcWMx7AdgysbHrjnd2ssJJXkrdHQV1P/vXtmbFsok4=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.5 h1:VWVDqUz2P9qQ4oarjkq3kfpn2KSkYNoosS2zWGM3luI=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.5/go.mod h1:vM0U7a/Exi1ziX/u9QCSuevrPgmH+qbhwDi81CfEHTw=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.5 h1:OR1FrOPrNISfOeYGXN3Tlj35meOpYkW0gdXQ2jvu4U0=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.5/go.mod h1:v+U73J9xr5bwN0IYXIUlQnfWGVrcIGlY1wuq+vDsCyg=
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.24.1 h1:VKo7a7XJbqzzELwaLzjpGm7uPPtO+ZKCs/oHonYk7lE=
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.24.1/go.mod h1:MQeMcZxXxWr8VHc/8f8Hspg5D5dCm7wchXHYaiBntDw=
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.9.1 h1:qrd9evf91w8s7TIIvJakNzRJLW7HRiMDpQ9nmI7cOgo=
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.9.1/go.mod h1:ckNYD0WIFojv8yOx5QiTp2tg64kTJdMFDPjR4y4mboY=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.5 h1:NUT8HreIoLAfPAP7eEYN8jwidvY+4TjZ86xqS/iGDQI=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.5/go.mod h1:0YrrF3m5qWi/EItvW0wmZCydlgr8lzJ882P2mXoHo7U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.7.0 h1:F1diQIOkNn8jcez4173r+PLPdkWK7chy74r3fKpDrLI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.7.0/go.mod h1:8ctElVINyp+SjhoZZceUAZw78glZH6R8ox5MVNu5j2s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2/go.mod h1:FgR1tCsn8C6+Hf+N5qkfrE4IXvUL1RgW87sunJ+5J4I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.11.0 h1:XAe+PDnaBELHr25qaJKfB415V4CKFWE8H+prUreql8k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.11.0/go.mod h1:RMlgnt1LbOT2BxJ3cdw+qVz7KL84714LFkWtF6sLI7A=
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.23.1 h1:sDA1G6xYGxD13Payuk1DHdL3WP5M+ThXnu8B/jtq49U=
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.23.1/go.mod h1:lKQt+LgUWCTmIqy+f42/QeususXmc6CbOtjnME6znCU=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.22.1 h1:XNhG6ji2v6JOxQ0al56bSXa2afL3fwDWIb4YJ29hjXs=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.22.1/go.mod h1:hY95ss9brlWlTJO5R7B0nW9+bNVu+UIDpz9V0ykPkW0=
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.1 h1:Uuz3+qqwuv2TbspZuaFE8oy3hiO1kJtBdOi/dCKkHRQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.1/go.mod h1:07r8Y/d1rfafUJnx1VgE0YiSh99pmvLHGOFM3TCULUo=
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.9.5 h1:S8LfapcSpMDJk1xTJVvMnXUpayLyHHlcume4bHNspwI=
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.9.5/go.mod h1:/6h1vNnYmAt6aDaNTslyzr1vrwWDUB9Cb0cjJd6QwFg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.15 h1:HCfNaFlg/ELql6jYHWoUfIbp74nrqY6+OJWvZjdvH7I=
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.15/go.mod h1:GS2CgHn+9w1d8emnS+2DVbQq2UBOKo9wsUaDEZTnVro=
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.16.0 h1:xYxIpmqlnc+U/miylJaNmEty34MC4BmxpVOqkF2DFpo=
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.16.0/go.mod h1:U1tzFmWLyt4AqSRLONL0RXcYsQg0huiInDdRmCecz1w=
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.25.0 h1:sD7lJ9EchUyaZXjXyBtK0TwaGULzTmtV1xo+wrNT6hw=
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.25.0/go.mod h1:7vdpNDFFcV8yDaDTPpwy5wv28w1815yVJxHaCX4hmmM=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.20.5 h1:Vex6D07/CmahT0LIaiRk+j9xyVAsvQxBa8iv38p3Ajc=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.20.5/go.mod h1:QZWV7sxHUg/qsPJcAtAI9JyLPKZ78weHmdILmYMCqEE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.24.0 h1:REKac2iT0HYxUSzqOSuncnmsZnE3m4MlGfo1dOUN3vg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.24.0/go.mod h1:oIUXg/5F0x0gy6nkwEnlxZboueddwPEKO6Xl+U6/3a0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.32.1 h1:nOjJ5PevzE1ymPIYnJk6X/+EkeL2aDP1E4HpJvCwlAQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.32.1/go.mod h1:nF9ZnJeXLYw8PPRAtsBbG3aY1P8c5bUH2zqA/IAzaks=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.13.0 h1:VKvs4yx3nrcyBJcj4iSy5UI/Awdsa0fbDKesiNwPuZY=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.13.0/go.mod h1:5Oibvfj4kc6CE70qamrlOU+KSO/JWANgxIVbesvSMCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.1 h1:w/HlW+NGK5EU5jf/qekDZ56kg9jhvP/1Egh3bMRTdgo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.1/go.mod h1:Ej87mQA2lDTOyPL/ZCjoChhTCU/fwPKg5Em62pOIqVc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk=
|
||||
github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
|
||||
github.com/aws/smithy-go v1.11.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/aws/smithy-go v1.12.1 h1:yQRC55aXN/y1W10HgwHle01DRuV9Dpf31iGkotjt3Ag=
|
||||
github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1 h1:02c72fDJr87N8RAC2s3Qu0YuvMRZKNZJ9F+lAehCazk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1/go.mod h1:JLnGeGONAyi2lWXI1p0PCIOIy333JMVK1U7Hf0aRFLw=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 h1:RKci2D7tMwpvGpDNZnGQw9wk6v7o/xSwFcUAuNPoB8k=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK8eoQJ5+aYE7PkK1K6v41qJ5pJdK3ggCDvg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3 h1:3kfBKcX3votFX84dm00U8RGA1sCCh3eRMOGzg5dCWfU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3 h1:ur+FHdp4NbVIv/49bUjBW+FE7e57HOo03ELodttmagk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 h1:E3PXZSI3F2bzyj6XxUXdTIfvp425HHhwKsFvmzBwHgs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 h1:nBO/RFxeq/IS5G9Of+ZrgucRciie2qpLy++3UGZ+q2E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19 h1:oRHDrwCTVT8ZXi4sr9Ld+EXk7N/KGssOr2ygNeojEhw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19/go.mod h1:6Q0546uHDp421okhmmGfbxzq2hBqbXFNpi4k+Q1JnQA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26 h1:Mza+vlnZr+fPKFKRq/lKGVvM6B/8ZZmNdEopOwSQLms=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26/go.mod h1:Y2OJ+P+MC1u1VKnavT+PshiEuGPyh/7DqxoDNij4/bg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16 h1:2EXB7dtGwRYIN3XQ9qwIW504DVbKIw3r89xQnonGdsQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16/go.mod h1:XH+3h395e3WVdd6T2Z3mPxuI+x/HVtdqVOREkTiyubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.25 h1:WCRj8JKifyZTZiFGY6ayyNypZOlHsPbCqv6Tqfjmn7s=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.25/go.mod h1:3olVANhEv+CFhEvC/TTkqh+1kg+r0px3CbH5eRKx7J4=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.23 h1:tRkvVyUDMi4DBV6+QKK9Vmi2QN/SHmmcQo21vACw1Tc=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.23/go.mod h1:BEIWaGqO27qq9JeFeY746S4+SFmBajpV+yhGne2qbMo=
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.15.0 h1:bX586RbPhYHIZKQ01cEUImiHU5y/7Ob5R9c5N6jhGuc=
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.15.0/go.mod h1:NgTcDA7LdWjNGGBhS09XMVuwr8MZSFy/4G+BwZASviE=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.24.1 h1:k8/rAveUOcZV8Ex6WiCmvdtTcDtyf0QgvMmVFA9PRJ8=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.24.1/go.mod h1:AyrrIfauUrYfHqLrnroijTBBegQow3QIZTaLbQsauNk=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.21.0 h1:3bV+nNw0PPxkHvdRWUogUMtikIgIo2bVj48MvxQvoHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.21.0/go.mod h1:fV9PzXIIT2xUvf5KXEGToDYxEXcqDyO33SRfv2rAs/8=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.20.3 h1:syWZB25hdMayklHlAe195ertSget7wBEjExW/t+tfJ0=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.20.3/go.mod h1:G3xZtg7cjsJaJdl1oVkscYXbdDLZBfOHbE1JqcnZxOI=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.7 h1:/TwGWNd3vnjXaPMau8eY7s5j6Afe4WxnRfIB64r4jEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.7/go.mod h1:BiglbKCG56L8tmMnUEyEQo422BO9xnNR8vVHnOsByf8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.72.1 h1:iR8DtI9Jc9sMdOsvjiu6rs5jH+9csW88elgwpEMP8TU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.72.1/go.mod h1:zul71QqzR4D1a90/5FloZiAnZ1CtuIjVH7R9MP997+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.23 h1:Da25Cw8xI2q9Gr+ekQvpqJpyqR61xcb9Fh1Tkr/Cv/w=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.23/go.mod h1:kEVGiy2tACP0cegVqx4MrjsgQMSgrtgRq1fSa+Ix6F0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.20.0 h1:tFOfeyENzdOM5zKwGXdDsqeVJEjav8YLTb7tDehJ/c0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.20.0/go.mod h1:NpR78BP2STxvF/R1GXLDM4gAEfjz68W/h0nC5b6Jk3s=
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.18.0 h1:06pqrWeT8jrj8NIE0kovrFY3KC0pBt913HyusYyFjm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.18.0/go.mod h1:DdO6hLx0XiYuj2Tn8VsR8et3BW1aXiv74GfsjHvhN9U=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.23.0 h1:roxmtB8AB5b/COvsD75AnswtsQOxqbjjvowmnX8IKDE=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.23.0/go.mod h1:bxjOnpk0lwAq4jmmTONfUGPjgO8sLAbflxTHsY7thkU=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.23 h1:wJdl2doHQU+60ETqLHfXtY4ywmm36ZHiinA/rVkTtl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.23/go.mod h1:gHgx63dm28U/o5NotzfFWhxUASzcZuPgO8GDpsI1Aqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.26 h1:gAy84CkCnK2vgh7Rj5tRzpqeATCPShVCIjJZ0g7iKNg=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.26/go.mod h1:uIsRP+M5F/Ch+21isqTg6u16FXl2yzupCX0Dli4eQEM=
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.25.4 h1:cqb2+5MxHa+qdNWiu4giFuDdgBlyDzDawiROMYTuzBM=
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.25.4/go.mod h1:dOw+yZ4MsogGQIWd7z6jNb0Q9aSlSeTy3EZWKDGT/dM=
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.35.0 h1:qs2mr/MDqcAkG2XimkX3PcwR5jwCCZ8r4/yAWOCjGJo=
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.35.0/go.mod h1:kgD6fBlQEkhJlffBbS8SGYtpjboavr9e8B1ZouD84pY=
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.10.0 h1:Oi4le4KruAPyOzM5eIIUfUHMKnFKpyGAt5sglp+KIgw=
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.10.0/go.mod h1:mvMvy3rVAodoUa9hx6vS8cooFkLlpkt46n7BWVZNyA0=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.23 h1:HOtW30EkfQevdv++mKguMyn8/agh1z2VuBGR4Hou/u8=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.23/go.mod h1:yQ92mKfw/Gg5AvgxGmfdufKEyVoa9RNBsdnB9j5Gzkk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10 h1:dpiPHgmFstgkLG07KaYAewvuptq5kvo52xn7tVSrtrQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10/go.mod h1:9cBNUHI2aW4ho0A5T87O294iPDuuUOSIEDjnd1Lq/z0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.20 h1:KSvtm1+fPXE0swe9GPjc6msyrdTT0LB/BP8eLugL1FI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.20/go.mod h1:Mp4XI/CkWGD79AQxZ5lIFlgvC0A+gl+4BmyG1F+SfNc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.19 h1:V03dAtcAN4Qtly7H3/0B6m3t/cyl4FgyKFqK738fyJw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.19/go.mod h1:2WpVWFC5n4DYhjNXzObtge8xfgId9UP6GWca46KJFLo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19 h1:GE25AWCdNUPh9AOJzI9KIJnja7IwUc1WyUqz/JTyJ/I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19/go.mod h1:02CP6iuYP+IVnBX5HULVdSAku/85eHB2Y9EsFhrkEwU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19 h1:piDBAaWkaxkkVV3xJJbTehXCZRXYs49kvpi/LG6LR2o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19/go.mod h1:BmQWRVkLTmyNzYPFAZgon53qKLWBNSvonugD1MrSWUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.25.1 h1:mmWZ5Hdc7hMHIUVLgOFLjQQvvUGZ3uhNO8mDGznqIiA=
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.25.1/go.mod h1:2oqKd3SCTyhVaUei20xDUOOcqOAuAnbCy79w/t1dDVs=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.24.0 h1:ILjBjigugB2APXvhntZU+XwDAcaTId22dE0OwIbi/2A=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.24.0/go.mod h1:LYCAGTJ4XgNZAESKxLC9L12F7IIQbJHccVxanY8DYzo=
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.15 h1:K0UbxNfaPHoIXo0T4L4jrbE+TKcVPYp55nEnMLxVRPk=
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.15/go.mod h1:ycghPMXYItx5Y74iehFgGwDNUMXdq0xCxLaYC5uYZO0=
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.11.3 h1:ViGcWux+UbkS/JBLxPILq/Mevlf9RXSycGlPaVQON5Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.11.3/go.mod h1:7mh4ks7MsQUuhHFCr10LIF9gKVS+0yylPUmCEM/kzJE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.24 h1:pzB33ZEhd1szVKHkgYe4cXQ+kSLJEFGBLy+IpbAKY9Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.24/go.mod h1:el3+/AiwdwKk5qZEcCQqm+t3b1A5QLZI2nr/OkhM/gc=
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.31.0 h1:lx8pflhN7oogISvAorwqPGFHN7eiVlGEwk21ThYcyoA=
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.31.0/go.mod h1:wPFe1Cj3nZWmNWKKdkXw961l1dJheTZQ5JjPImqbMuI=
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.26.16 h1:qPa2cNgLUAMavzTD9joFfjU/ue5GE1gFsJPLD13yomw=
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.26.16/go.mod h1:Ds+ElMoF3oxjYWJZofKeNeih3q3W7vzRCkRdnu/aOj8=
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.13.24 h1:CP4LqvOiNVTtWoUgISmhTzUinT7zvYd57C6Xey9w86k=
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.13.24/go.mod h1:/WfhDm5Hmfy/3TSM/1m9ojM0IQsBuVGvd3vITQc86i0=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.25.0 h1:ubppi63qDFs3J7cg8uDOzyvlmKFQDoxL2tlHb7mfbR8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.25.0/go.mod h1:kUSK8EkGYdzFbTmADk0t7yRIoESH80xjWe8Bp6dQce8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4 h1:QgmmWifaYZZcpaw3y1+ccRlgH6jAvLm4K/MBGUc7cNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.55.0 h1:i0G6pXYPs6FsufUBOGXqE/ozlFxZUf+pwp5fhSEvs7E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.55.0/go.mod h1:umigFBRAb3i0OTRXRYDLlCawnJPYCWLhod55+tZHOFA=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8 h1:Zw48FHykP40fKMxPmagkuzklpEuDPLhvUjKP8Ygrds0=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6 h1:rfQqunscpnVmvK6O9B2DwrBzIMICSCKswPwkD2XDan8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6/go.mod h1:2cPUjR63iE9MPMPJtSyzYmsTFCNrN/Xi9j0v9BL5OU0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15 h1:5PgOVgJWObGxve+0qU7T/C0reU6RxqpNwbuunLT9Vlc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15/go.mod h1:DKX/7/ZiAzHO6p6AhArnGdrV4r+d461weby8KeVtvC4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1 h1:N4aPQGoAgdUr+3F1UcuW8/WE3aM7sxzOpzDP0hWkJCg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1/go.mod h1:rEsqsZrOp9YvSGPOrcL3pR9+i/QJaWRkAYbuxMa7yCU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25 h1:GFZitO48N/7EsFDt8fMa5iYdmWqkUDDB3Eje6z3kbG0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 h1:jcw6kKZrtNfBPJkaHrscDOZoe5gvi9wjudnxvozYFJo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5 h1:60SJ4lhvn///8ygCzYy2l53bFW/Q15bVfyjyAWo6zuw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
||||
github.com/aws/smithy-go v1.13.4 h1:/RN2z1txIJWeXeOkzX+Hk/4Uuvv7dWtCjbmVJcrskyk=
|
||||
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/bishopfox/awsservicemap v1.0.0 h1:iZOPsMPfMmugLhOctLJUqcCrkT5+NflF5DIv7EFomMs=
|
||||
github.com/bishopfox/awsservicemap v1.0.0/go.mod h1:oy9Fyqh6AozQjShSx+zRNouTlp7k3z3YEMoFkN8rquc=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
@@ -180,14 +183,15 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/dominikbraun/graph v0.15.0 h1:U6N0fWJTDsUO9R5NhFE1Xi0YGGQ1+nKV9KO5apClf3E=
|
||||
github.com/dominikbraun/graph v0.15.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -196,17 +200,18 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8=
|
||||
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||
github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc=
|
||||
github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk=
|
||||
github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o=
|
||||
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -244,8 +249,6 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@@ -272,8 +275,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
@@ -291,17 +294,20 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
|
||||
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
@@ -311,21 +317,21 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
|
||||
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
|
||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
@@ -344,8 +350,9 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg=
|
||||
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
|
||||
go.mongodb.org/mongo-driver v1.11.0 h1:FZKhBSTydeuffHj9CBjXlR8vQLee1cQyTWYPA6/tqiE=
|
||||
go.mongodb.org/mongo-driver v1.11.0/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -357,12 +364,14 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -458,7 +467,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -486,11 +494,13 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -498,8 +508,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package utils
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/retry"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/kyokomi/emoji"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var (
|
||||
TxtLoggerName = "root"
|
||||
TxtLog = TxtLogger()
|
||||
UtilsFs = afero.NewOsFs()
|
||||
)
|
||||
|
||||
func AWSConfigFileLoader(AWSProfile string, version string) aws.Config {
|
||||
// Ensures the profile in the aws config file meets all requirements (valid keys and a region defined). I noticed some calls fail without a default region.
|
||||
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(AWSProfile), config.WithDefaultRegion("us-east-1"), config.WithRetryer(
|
||||
func() aws.Retryer {
|
||||
return retry.AddWithMaxAttempts(retry.NewStandard(), 3)
|
||||
}))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
TxtLog.Println(err)
|
||||
}
|
||||
|
||||
_, err = cfg.Credentials.Retrieve(context.TODO())
|
||||
if err != nil {
|
||||
fmt.Printf("[%s][%s] Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile))
|
||||
TxtLog.Printf("Could not retrieve the specified profile name %s", err)
|
||||
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func AWSWhoami(awsProfile string, version string) (*sts.GetCallerIdentityOutput, error) {
|
||||
// Connects to STS and checks caller identity. Same as running "aws sts get-caller-identity"
|
||||
//fmt.Printf("[%s] Retrieving caller's identity\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)))
|
||||
STSService := sts.NewFromConfig(AWSConfigFileLoader(awsProfile, version))
|
||||
CallerIdentity, err := STSService.GetCallerIdentity(context.TODO(), &sts.GetCallerIdentityInput{})
|
||||
if err != nil {
|
||||
fmt.Printf("[%s][%s] Could not get caller's identity\n\nError: %s\n\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(awsProfile), err)
|
||||
TxtLog.Printf("Could not get caller's identity: %s", err)
|
||||
return CallerIdentity, err
|
||||
|
||||
}
|
||||
return CallerIdentity, err
|
||||
}
|
||||
|
||||
func GetEnabledRegions(awsProfile string, version string) []string {
|
||||
var enabledRegions []string
|
||||
ec2Client := ec2.NewFromConfig(AWSConfigFileLoader(awsProfile, version))
|
||||
regions, err := ec2Client.DescribeRegions(
|
||||
context.TODO(),
|
||||
&ec2.DescribeRegionsInput{
|
||||
AllRegions: aws.Bool(false),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
AWSRegions, err := servicemap.GetAllRegions()
|
||||
if err != nil {
|
||||
TxtLog.Println(err)
|
||||
}
|
||||
return AWSRegions
|
||||
}
|
||||
|
||||
for _, region := range regions.Regions {
|
||||
enabledRegions = append(enabledRegions, *region.RegionName)
|
||||
}
|
||||
|
||||
return enabledRegions
|
||||
|
||||
}
|
||||
|
||||
// func GetRegionsForService(awsProfile string, service string) []string {
|
||||
// SSMClient := ssm.NewFromConfig(AWSConfigFileLoader(awsProfile))
|
||||
// var PaginationControl *string
|
||||
// var supportedRegions []string
|
||||
// path := fmt.Sprintf("/aws/service/global-infrastructure/services/%s/regions", service)
|
||||
|
||||
// ServiceRegions, err := SSMClient.GetParametersByPath(
|
||||
// context.TODO(),
|
||||
// &(ssm.GetParametersByPathInput{
|
||||
// NextToken: PaginationControl,
|
||||
// Path: &path,
|
||||
// }),
|
||||
// )
|
||||
// if err != nil {
|
||||
// fmt.Println(err.Error())
|
||||
|
||||
// }
|
||||
|
||||
// if ServiceRegions.Parameters != nil {
|
||||
// for _, region := range ServiceRegions.Parameters {
|
||||
// name := *region.Value
|
||||
// supportedRegions = append(supportedRegions, name)
|
||||
// }
|
||||
|
||||
// // The "NextToken" value is nil when there's no more data to return.
|
||||
// if ServiceRegions.NextToken != nil {
|
||||
// PaginationControl = ServiceRegions.NextToken
|
||||
// } else {
|
||||
// PaginationControl = nil
|
||||
// }
|
||||
// }
|
||||
// return supportedRegions
|
||||
// }
|
||||
|
||||
// txtLogger - Returns the txt logger
|
||||
func TxtLogger() *logrus.Logger {
|
||||
txtLogger := logrus.New()
|
||||
txtFile, err := os.OpenFile(fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath())), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to open log file %v", err))
|
||||
}
|
||||
txtLogger.Out = txtFile
|
||||
txtLogger.SetLevel(logrus.InfoLevel)
|
||||
//txtLogger.SetReportCaller(true)
|
||||
|
||||
return txtLogger
|
||||
}
|
||||
|
||||
func CheckErr(e error, msg string) {
|
||||
if e != nil {
|
||||
TxtLog.Printf("[-] Error %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func GetAllAWSProfiles(AWSConfirm bool) []string {
|
||||
var AWSProfiles []string
|
||||
|
||||
credentialsFile, err := UtilsFs.Open(config.DefaultSharedCredentialsFilename())
|
||||
CheckErr(err, "could not open default AWS credentials file")
|
||||
if err == nil {
|
||||
defer credentialsFile.Close()
|
||||
scanner := bufio.NewScanner(credentialsFile)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(text, "[") && strings.HasSuffix(text, "]") {
|
||||
text = strings.TrimPrefix(text, "[")
|
||||
text = strings.TrimSuffix(text, "]")
|
||||
if !Contains(text, AWSProfiles) {
|
||||
AWSProfiles = append(AWSProfiles, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configFile, err := UtilsFs.Open(config.DefaultSharedConfigFilename())
|
||||
CheckErr(err, "could not open default AWS credentials file")
|
||||
if err == nil {
|
||||
defer configFile.Close()
|
||||
scanner2 := bufio.NewScanner(configFile)
|
||||
scanner2.Split(bufio.ScanLines)
|
||||
for scanner2.Scan() {
|
||||
text := strings.TrimSpace(scanner2.Text())
|
||||
if strings.HasPrefix(text, "[") && strings.HasSuffix(text, "]") {
|
||||
text = strings.TrimPrefix(text, "[profile ")
|
||||
text = strings.TrimPrefix(text, "[")
|
||||
text = strings.TrimSuffix(text, "]")
|
||||
if !Contains(text, AWSProfiles) {
|
||||
AWSProfiles = append(AWSProfiles, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !AWSConfirm {
|
||||
result := ConfirmSelectedProfiles(AWSProfiles)
|
||||
if !result {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
return AWSProfiles
|
||||
|
||||
}
|
||||
|
||||
func ConfirmSelectedProfiles(AWSProfiles []string) bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Printf("[ %s] Identified profiles:\n\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")))
|
||||
for _, profile := range AWSProfiles {
|
||||
fmt.Printf("\t* %s\n", profile)
|
||||
}
|
||||
fmt.Printf("\n[ %s] Are you sure you'd like to run this command against the [%d] listed profile(s)? (Y\\n): ", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), len(AWSProfiles))
|
||||
text, _ := reader.ReadString('\n')
|
||||
switch text {
|
||||
case "\n", "Y\n", "y\n":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func GetSelectedAWSProfiles(AWSProfilesListPath string) []string {
|
||||
AWSProfilesListFile, err := UtilsFs.Open(AWSProfilesListPath)
|
||||
CheckErr(err, fmt.Sprintf("could not open given file %s", AWSProfilesListPath))
|
||||
if err != nil {
|
||||
fmt.Printf("\nError loading profiles. Could not open file at location[%s]\n", AWSProfilesListPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer AWSProfilesListFile.Close()
|
||||
var AWSProfiles []string
|
||||
scanner := bufio.NewScanner(AWSProfilesListFile)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
profile := strings.TrimSpace(scanner.Text())
|
||||
if len(profile) != 0 {
|
||||
AWSProfiles = append(AWSProfiles, profile)
|
||||
}
|
||||
}
|
||||
return AWSProfiles
|
||||
}
|
||||
|
||||
func removeBadPathChars(receivedPath *string) string {
|
||||
var path string
|
||||
var bannedPathChars *regexp.Regexp = regexp.MustCompile(`[<>:"'|?*]`)
|
||||
path = bannedPathChars.ReplaceAllString(aws.ToString(receivedPath), "_")
|
||||
|
||||
return path
|
||||
|
||||
}
|
||||
|
||||
func BuildAWSPath(Caller sts.GetCallerIdentityOutput) string {
|
||||
var callerAccount = removeBadPathChars(Caller.Account)
|
||||
var callerUserID = removeBadPathChars(Caller.UserId)
|
||||
|
||||
return fmt.Sprintf("%s-%s", callerAccount, callerUserID)
|
||||
}
|
||||
|
||||
// this is all for the spinner and command counter
|
||||
const clearln = "\r\x1b[2K"
|
||||
|
||||
type CommandCounter struct {
|
||||
Total int
|
||||
Pending int
|
||||
Complete int
|
||||
Error int
|
||||
Executing int
|
||||
}
|
||||
|
||||
func SpinUntil(callingModuleName string, counter *CommandCounter, done chan bool, spinType string) {
|
||||
defer close(done)
|
||||
for {
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
fmt.Printf(clearln+"[%s] Status: %d/%d %s complete (%d errors -- For details check %s)", cyan(callingModuleName), counter.Complete, counter.Total, spinType, counter.Error, fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath())))
|
||||
case <-done:
|
||||
fmt.Printf(clearln+"[%s] Status: %d/%d %s complete (%d errors -- For details check %s)\n", cyan(callingModuleName), counter.Complete, counter.Complete, spinType, counter.Error, fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath())))
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func compareSlice(a []string, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for idx, elem := range a {
|
||||
if elem != b[idx] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Case empty file.
|
||||
// Case expected format.
|
||||
func TestGetAllAWSProfiles(t *testing.T) {
|
||||
var tests = []struct {
|
||||
fileData []byte
|
||||
expectedOutput []string
|
||||
AWSConfirm bool
|
||||
caseName string
|
||||
}{
|
||||
{[]byte(""), []string{}, true, "empty file"},
|
||||
{[]byte("[default]\naws_access_key=abc\naws_secret_access_key=123\n[123]\naws_access_key=abc\naws_secret_access_key=123"), []string{"default", "123"}, true, "expected format"},
|
||||
}
|
||||
credentialsFile := config.DefaultSharedCredentialsFilename()
|
||||
|
||||
for _, test := range tests {
|
||||
UtilsFs = afero.NewMemMapFs()
|
||||
fmt.Printf("[*] Testing %s\n", test.caseName)
|
||||
UtilsFs.Create(credentialsFile)
|
||||
afero.WriteFile(UtilsFs, credentialsFile, test.fileData, 0755)
|
||||
output := GetAllAWSProfiles(test.AWSConfirm)
|
||||
if !compareSlice(output, test.expectedOutput) {
|
||||
t.Errorf("Test Failed: %v inputted, %v expected, received: %v", test.fileData, test.expectedOutput, output)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Case empty file.
|
||||
// Case special characters \r \n \t.
|
||||
// Case with extra new lines at the end.
|
||||
// Case expected format.
|
||||
func TestGetSelectedAWSProfiles(t *testing.T) {
|
||||
var tests = []struct {
|
||||
fileData []byte
|
||||
expectedOutput []string
|
||||
AWSConfirm bool
|
||||
caseName string
|
||||
}{
|
||||
{[]byte(""), []string{}, true, "empty file"},
|
||||
{[]byte("abcd\r\nxyz\t\n123\r\t"), []string{"abcd", "xyz", "123"}, true, "special characters \\r \\n \\t"},
|
||||
{[]byte("qwerty\nxyz\n456\n\n\n"), []string{"qwerty", "xyz", "456"}, true, "extra new lines at the end"},
|
||||
{[]byte("nmhj\nyuioy\n098"), []string{"nmhj", "yuioy", "098"}, true, "expected format"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
UtilsFs = afero.NewMemMapFs()
|
||||
fmt.Printf("[*] Testing %s\n", test.caseName)
|
||||
UtilsFs.Create("/tmp/myfile.txt")
|
||||
afero.WriteFile(UtilsFs, "/tmp/myfile.txt", test.fileData, 0755)
|
||||
output := GetSelectedAWSProfiles("/tmp/myfile.txt")
|
||||
if !compareSlice(output, test.expectedOutput) {
|
||||
t.Errorf("Test Failed: %v inputted, %v expected, recieved: %v", test.fileData, test.expectedOutput, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
|
||||
"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/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage"
|
||||
"github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
)
|
||||
|
||||
func getAuthorizer(endpoint string) (autorest.Authorizer, error) {
|
||||
auth, err := auth.NewAuthorizerFromCLIWithResource(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get client authorizer: %s", err)
|
||||
}
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func GetTenantsClient() subscriptions.TenantsClient {
|
||||
client := subscriptions.NewTenantsClient()
|
||||
a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get subscriptions client: %s", err)
|
||||
}
|
||||
client.Authorizer = a
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetSubscriptionsClient() subscriptions.Client {
|
||||
client := subscriptions.NewClient()
|
||||
a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get subscriptions client: %s", err)
|
||||
}
|
||||
client.Authorizer = a
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetResourceGroupsClient(subscriptionID string) resources.GroupsClient {
|
||||
client := resources.NewGroupsClient(subscriptionID)
|
||||
a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get resource groups client: %s", err)
|
||||
}
|
||||
client.Authorizer = a
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetAADUsersClient(tenantID string) graphrbac.UsersClient {
|
||||
client := graphrbac.NewUsersClient(tenantID)
|
||||
a, err := getAuthorizer(globals.AZ_GRAPH_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get azure active directory client: %s", err)
|
||||
}
|
||||
client.Authorizer = a
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetRoleAssignmentsClient(subscriptionID string) authorization.RoleAssignmentsClient {
|
||||
client := authorization.NewRoleAssignmentsClient(subscriptionID)
|
||||
a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get role assignments client: %s", err)
|
||||
}
|
||||
client.Authorizer = a
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetRoleDefinitionsClient(subscriptionName string) authorization.RoleDefinitionsClient {
|
||||
client := authorization.NewRoleDefinitionsClient(subscriptionName)
|
||||
a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get role definitions client: %s", err)
|
||||
}
|
||||
client.Authorizer = a
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetVirtualMachinesClient(subscriptionID string) compute.VirtualMachinesClient {
|
||||
client := compute.NewVirtualMachinesClient(subscriptionID)
|
||||
authorizer, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get compute client: %s", err)
|
||||
}
|
||||
client.Authorizer = authorizer
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetNICClient(subscriptionID string) network.InterfacesClient {
|
||||
client := network.NewInterfacesClient(subscriptionID)
|
||||
authorizer, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get nic client: %s", err)
|
||||
}
|
||||
client.Authorizer = authorizer
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetPublicIPClient(subscriptionID string) network.PublicIPAddressesClient {
|
||||
client := network.NewPublicIPAddressesClient(subscriptionID)
|
||||
authorizer, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get public ip client: %s", err)
|
||||
}
|
||||
client.Authorizer = authorizer
|
||||
return client
|
||||
}
|
||||
|
||||
func GetStorageClient(subscriptionID string) storage.AccountsClient {
|
||||
client := storage.NewAccountsClient(subscriptionID)
|
||||
a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get storage client: %s", err)
|
||||
}
|
||||
client.Authorizer = a
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
)
|
||||
|
||||
// Requires Az CLI Authentication to passs
|
||||
func TestGetAuthorizer(t *testing.T) {
|
||||
t.Skip()
|
||||
subtests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
}{
|
||||
{
|
||||
name: "Resource Manager Authorizer",
|
||||
endpoint: globals.AZ_RESOURCE_MANAGER_ENDPOINT,
|
||||
},
|
||||
{
|
||||
name: "Graph API Authorizer",
|
||||
endpoint: globals.AZ_GRAPH_ENDPOINT,
|
||||
},
|
||||
}
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
log.Printf("Test case: %s", subtest.name)
|
||||
authorizer, err := getAuthorizer(subtest.endpoint)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
log.Print(authorizer)
|
||||
}
|
||||
fmt.Println()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/jedib0t/go-pretty/text"
|
||||
)
|
||||
|
||||
func init() {
|
||||
text.EnableColors()
|
||||
}
|
||||
|
||||
// This function returns ~/.cloudfox.
|
||||
// If the folder does not exist the function creates it.
|
||||
func GetLogDirPath() *string {
|
||||
user, _ := user.Current()
|
||||
dir := filepath.Join(user.HomeDir, globals.CLOUDFOX_LOG_FILE_DIR_NAME)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to read or create cloudfox directory")
|
||||
}
|
||||
}
|
||||
return ptr.String(dir)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package utils
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package utils
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/term"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// Used for file system mocking with Afero library. Set:
|
||||
@@ -38,24 +38,26 @@ type OutputData2 struct {
|
||||
// verbosity = 2 (Output and loot printed to file, output printed screen).
|
||||
// verbosity = 3 (Output and loot printed to file and screen).
|
||||
// outputType = "table", "csv"
|
||||
func OutputSelector(verbosity int, outputType string, header []string, body [][]string, outputDirectory string, fileName string, callingModule string) {
|
||||
// prefixIdentifier = this string gets printed with control message calling module (e.g. aws profile, azure resource group, gcp project, etc)
|
||||
func OutputSelector(verbosity int, outputType string, header []string, body [][]string, outputDirectory string, fileName string, callingModule string, wrapTable bool, prefixIdentifier string) {
|
||||
|
||||
switch verbosity {
|
||||
case 2:
|
||||
printTableToScreen(header, body)
|
||||
PrintTableToScreen(header, body, wrapTable)
|
||||
case 3:
|
||||
printTableToScreen(header, body)
|
||||
PrintTableToScreen(header, body, wrapTable)
|
||||
// Add writeLootToScreen function here
|
||||
}
|
||||
switch outputType {
|
||||
case "table":
|
||||
fmt.Println("")
|
||||
outputFileTable := createOutputFile(
|
||||
ptr.String(filepath.Join(outputDirectory, "table")),
|
||||
ptr.String(fmt.Sprintf("%s.txt", fileName)),
|
||||
outputType,
|
||||
callingModule)
|
||||
printTableToFile(header, body, outputFileTable)
|
||||
fmt.Printf("[%s] Output written to [%s]\n", cyan(callingModule), outputFileTable.Name())
|
||||
fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileTable.Name())
|
||||
// Add writeLootToFile function here
|
||||
|
||||
case "csv":
|
||||
@@ -65,8 +67,7 @@ func OutputSelector(verbosity int, outputType string, header []string, body [][]
|
||||
outputType,
|
||||
callingModule)
|
||||
printCSVtoFile(header, body, outputFileCSV)
|
||||
fmt.Printf("[%s] Output written to [%s]\n", cyan(callingModule), outputFileCSV.Name())
|
||||
|
||||
fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileCSV.Name())
|
||||
// Add writeLootToFile function here
|
||||
|
||||
default:
|
||||
@@ -76,7 +77,7 @@ func OutputSelector(verbosity int, outputType string, header []string, body [][]
|
||||
outputType,
|
||||
callingModule)
|
||||
printTableToFile(header, body, outputFileTable)
|
||||
fmt.Printf("[%s] Output written to [%s]\n", cyan(callingModule), outputFileTable.Name())
|
||||
fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileTable.Name())
|
||||
|
||||
outputFileCSV := createOutputFile(
|
||||
ptr.String(filepath.Join(outputDirectory, "csv")),
|
||||
@@ -84,19 +85,9 @@ func OutputSelector(verbosity int, outputType string, header []string, body [][]
|
||||
outputType,
|
||||
callingModule)
|
||||
printCSVtoFile(header, body, outputFileCSV)
|
||||
fmt.Printf("[%s] Output written to [%s]\n", cyan(callingModule), outputFileCSV.Name())
|
||||
fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileCSV.Name())
|
||||
// Add writeLootToFile function here
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func printCSVtoScreen(header []string, body [][]string) {
|
||||
csvWriter := csv.NewWriter(os.Stdout)
|
||||
csvWriter.Write(header)
|
||||
for _, row := range body {
|
||||
csvWriter.Write(row)
|
||||
}
|
||||
csvWriter.Flush()
|
||||
}
|
||||
|
||||
func printCSVtoFile(header []string, body [][]string, outputFile afero.File) {
|
||||
@@ -110,7 +101,6 @@ func printCSVtoFile(header []string, body [][]string, outputFile afero.File) {
|
||||
|
||||
func printTableToFile(header []string, body [][]string, outputFile afero.File) {
|
||||
t := table.New(outputFile)
|
||||
t.SetColumnMaxWidth(1000)
|
||||
t.SetHeaders(header...)
|
||||
t.AddRows(body...)
|
||||
t.SetRowLines(false)
|
||||
@@ -118,12 +108,21 @@ func printTableToFile(header []string, body [][]string, outputFile afero.File) {
|
||||
t.Render()
|
||||
}
|
||||
|
||||
func printTableToScreen(header []string, body [][]string) {
|
||||
func PrintTableToScreen(header []string, body [][]string, wrapLines bool) {
|
||||
standardColumnWidth := 1000
|
||||
t := table.New(os.Stdout)
|
||||
// ColumnMaxWidth needs to be set as a large value so the table doesn't wrap.
|
||||
// If the table wraps it's hard to grep the output from the terminal.
|
||||
// TO-DO: add a flag to make this optional.
|
||||
t.SetColumnMaxWidth(1000)
|
||||
if wrapLines {
|
||||
terminalWidth, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil {
|
||||
fmt.Println("error getting terminal size:", err)
|
||||
return
|
||||
}
|
||||
columnCount := len(header)
|
||||
// The offset value was defined by trial and error to get the best wrapping
|
||||
trialAndErrorOffset := 1
|
||||
standardColumnWidth = terminalWidth / (columnCount + trialAndErrorOffset)
|
||||
}
|
||||
t.SetColumnMaxWidth(standardColumnWidth)
|
||||
t.SetHeaders(header...)
|
||||
t.AddRows(body...)
|
||||
t.SetHeaderStyle(table.StyleBold)
|
||||
@@ -133,18 +132,6 @@ func printTableToScreen(header []string, body [][]string) {
|
||||
t.Render()
|
||||
}
|
||||
|
||||
func getTerminalWidth() (int, error) {
|
||||
terminalFileDescriptor := int(os.Stdout.Fd())
|
||||
if term.IsTerminal(terminalFileDescriptor) {
|
||||
width, _, err := term.GetSize(terminalFileDescriptor)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("[-] Valid terminal but failed to read width")
|
||||
}
|
||||
return width, nil
|
||||
}
|
||||
return 0, fmt.Errorf("invalid terminal")
|
||||
}
|
||||
|
||||
// The Afero library enables file system mocking:
|
||||
// fileSystem = afero.NewOsFs() if not unit testing (real file system) OR
|
||||
// fileSystem = afero.NewMemMapFs() for a mocked file system (when unit testing)
|
||||
@@ -171,3 +158,12 @@ func createOutputFile(outputDirectory *string, fileName *string, outputType stri
|
||||
//fmt.Printf("[%s] Creating output file: %s\n", cyan(callingModule), outputFile.Name())
|
||||
return outputFile
|
||||
}
|
||||
|
||||
func MockFileSystem(switcher bool) {
|
||||
if switcher {
|
||||
fmt.Println("Using mocked file system")
|
||||
fileSystem = afero.NewMemMapFs()
|
||||
} else {
|
||||
fmt.Println("Using OS file system. Make sure to clean up your disk!")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package utils
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestOutputSelector(t *testing.T) {
|
||||
@@ -17,6 +16,7 @@ func TestOutputSelector(t *testing.T) {
|
||||
outputDirectory string
|
||||
fileNameExtension string
|
||||
callingModule string
|
||||
prefixIdentifier string
|
||||
}{
|
||||
{
|
||||
name: "Verbosity1-OutputTable",
|
||||
@@ -25,6 +25,7 @@ func TestOutputSelector(t *testing.T) {
|
||||
outputDirectory: "cloudfox-output",
|
||||
fileNameExtension: ".txt",
|
||||
callingModule: "calling_module_1",
|
||||
prefixIdentifier: "AWS_PROFILE_1",
|
||||
},
|
||||
{
|
||||
name: "Verbosity2-OutputTable",
|
||||
@@ -33,6 +34,7 @@ func TestOutputSelector(t *testing.T) {
|
||||
outputDirectory: "cloudfox-output",
|
||||
fileNameExtension: ".txt",
|
||||
callingModule: "calling_module_2",
|
||||
prefixIdentifier: "AWS_PROFILE_2",
|
||||
},
|
||||
{
|
||||
name: "Verbosity3-OutputTable",
|
||||
@@ -41,6 +43,7 @@ func TestOutputSelector(t *testing.T) {
|
||||
outputDirectory: "cloudfox-output",
|
||||
fileNameExtension: ".txt",
|
||||
callingModule: "calling_module_3",
|
||||
prefixIdentifier: "AZURE_RESOURCE_GROUP_1",
|
||||
},
|
||||
{
|
||||
name: "Verbosity1-OutputCSV",
|
||||
@@ -49,6 +52,7 @@ func TestOutputSelector(t *testing.T) {
|
||||
outputDirectory: "cloudfox-output",
|
||||
fileNameExtension: ".csv",
|
||||
callingModule: "calling_module_4",
|
||||
prefixIdentifier: "AZURE_RESOURCE_GROUP_2",
|
||||
},
|
||||
{
|
||||
name: "Verbosity2-OutputCSV",
|
||||
@@ -57,6 +61,7 @@ func TestOutputSelector(t *testing.T) {
|
||||
outputDirectory: "cloudfox-output",
|
||||
fileNameExtension: ".csv",
|
||||
callingModule: "calling_module_5",
|
||||
prefixIdentifier: "GCP_PROJECT_1",
|
||||
},
|
||||
{
|
||||
name: "Verbosity3-OutputCSV",
|
||||
@@ -65,11 +70,12 @@ func TestOutputSelector(t *testing.T) {
|
||||
outputDirectory: "cloudfox-output",
|
||||
fileNameExtension: ".csv",
|
||||
callingModule: "calling_module_6",
|
||||
prefixIdentifier: "GCP_PROJECT_2",
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("TEST_CASE: CreateOutputFile")
|
||||
mockFileSystem(true)
|
||||
MockFileSystem(true)
|
||||
|
||||
for _, s := range subTests {
|
||||
fmt.Printf("\n[subtest]: %s\n", s.name)
|
||||
@@ -82,7 +88,7 @@ func TestOutputSelector(t *testing.T) {
|
||||
{"2020", "March"},
|
||||
{"2019", "April"},
|
||||
}
|
||||
OutputSelector(s.verbosity, s.outputType, header, body, s.outputDirectory, fmt.Sprintf("%s%s", s.callingModule, s.fileNameExtension), s.callingModule)
|
||||
OutputSelector(s.verbosity, s.outputType, header, body, s.outputDirectory, fmt.Sprintf("%s%s", s.callingModule, s.fileNameExtension), s.callingModule, false, s.prefixIdentifier)
|
||||
})
|
||||
}
|
||||
fmt.Println()
|
||||
@@ -116,7 +122,7 @@ func TestCreateOutputFile(t *testing.T) {
|
||||
}
|
||||
|
||||
fmt.Println("TEST_CASE: CreateOutputFile")
|
||||
mockFileSystem(true)
|
||||
MockFileSystem(true)
|
||||
|
||||
for _, s := range subTests {
|
||||
fmt.Printf("\n[subtest]: %s\n", s.name)
|
||||
@@ -131,11 +137,39 @@ func TestCreateOutputFile(t *testing.T) {
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func mockFileSystem(switcher bool) {
|
||||
if switcher {
|
||||
fmt.Println("Using mocked file system")
|
||||
fileSystem = afero.NewMemMapFs()
|
||||
} else {
|
||||
fmt.Println("Using OS file system. Make sure to clean up your disk!")
|
||||
func TestPrintTableToScreen(t *testing.T) {
|
||||
subTests := []struct {
|
||||
name string
|
||||
wrapLines bool
|
||||
}{
|
||||
{
|
||||
name: "wrapLines enabled",
|
||||
wrapLines: true,
|
||||
},
|
||||
{
|
||||
name: "wrapLines disabled",
|
||||
wrapLines: false,
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("TEST_CASE: PrintTableToScreen")
|
||||
|
||||
for _, s := range subTests {
|
||||
fmt.Printf("\n[subtest]: %s\n", s.name)
|
||||
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
header := []string{"A", "B", "C", "E", "F"}
|
||||
body := [][]string{
|
||||
{
|
||||
"AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA",
|
||||
"BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB",
|
||||
"DDDDDDDD-DDDD-DDDD-DDDD-DDDDDDDD",
|
||||
"EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEE",
|
||||
"FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFF",
|
||||
},
|
||||
}
|
||||
PrintTableToScreen(header, body, s.wrapLines)
|
||||
})
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -4,21 +4,17 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/BishopFox/cloudfox/cli"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: os.Args[0],
|
||||
Version: "1.7.0",
|
||||
Version: "1.9.0",
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
logfile := utils.InitLogging()
|
||||
defer logfile.Close()
|
||||
|
||||
rootCmd.AddCommand(cli.AWSCommands, cli.AzCommands)
|
||||
rootCmd.Execute()
|
||||
}
|
||||
@@ -5,12 +5,16 @@
|
||||
"Action": [
|
||||
"apigateway:GET",
|
||||
"apprunner:DescribeService",
|
||||
"apprunner:ListServices",
|
||||
"apprunner:ListServices",
|
||||
"cloudformation:DescribeStacks",
|
||||
"cloudformation:GetTemplate",
|
||||
"cloudformation:ListStacks",
|
||||
"cloudtrail:LookupEvents",
|
||||
"cloudfront:ListDistributions",
|
||||
"dynamodb:ListTables",
|
||||
"ec2:DescribeInstanceAttribute",
|
||||
"ec2:DescribeInstances",
|
||||
"ec2:DescribeNetworkInterfaces",
|
||||
"ec2:DescribeRegions",
|
||||
"ecr:DescribeImages",
|
||||
"ecr:DescribeRepositories",
|
||||
@@ -18,6 +22,10 @@
|
||||
"ecs:ListClusters",
|
||||
"ecs:ListTaskDefinitions",
|
||||
"ecs:ListTasks",
|
||||
"eks:DescribeCluster",
|
||||
"eks:DescribeNodegroup",
|
||||
"eks:ListClusters",
|
||||
"eks:ListNodegroups",
|
||||
"elasticfilesystem:DescribeFileSystemPolicy",
|
||||
"elasticfilesystem:DescribeFileSystems",
|
||||
"elasticfilesystem:DescribeMountTargets",
|
||||
@@ -27,6 +35,8 @@
|
||||
"elasticloadbalancing:DescribeListeners",
|
||||
"fsx:DescribeFileSystems",
|
||||
"fsx:DescribeVolumes",
|
||||
"glue:ListDevEndpoints",
|
||||
"glue:ListJobs",
|
||||
"grafana:ListWorkspaces",
|
||||
"iam:GetAccountAuthorizationDetails",
|
||||
"iam:ListAccessKeys",
|
||||
@@ -41,6 +51,8 @@
|
||||
"mq:ListBrokers",
|
||||
"es:DescribeDomain",
|
||||
"es:ListDomainNames",
|
||||
"ram:GetResourceShares",
|
||||
"ram:ListResources",
|
||||
"rds:DescribeDBInstances",
|
||||
"redshift:DescribeClusters",
|
||||
"route53:ListHostedZones",
|
||||
@@ -57,11 +69,15 @@
|
||||
"sagemaker:DescribeTrainingJob",
|
||||
"sagemaker:ListModels",
|
||||
"sagemaker:DescribeModel",
|
||||
"secretsmanager:ListSecrets"
|
||||
"secretsmanager:ListSecrets",
|
||||
"sns:ListTopics",
|
||||
"sqs:ListQueues",
|
||||
"tag:GetResources"
|
||||
|
||||
],
|
||||
"Resource": "*",
|
||||
"Effect": "Allow",
|
||||
"Sid": "AllowMoreReadForCloudFox"
|
||||
"Sid": "AllowMoreReadForCloudFox190"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/retry"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/kyokomi/emoji"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
TxtLoggerName = "root"
|
||||
TxtLogger = txtLogger()
|
||||
)
|
||||
|
||||
func AWSConfigFileLoader(AWSProfile string) aws.Config {
|
||||
// Ensures the profile in the aws config file meets all requirements (valid keys and a region defined). I noticed some calls fail without a default region.
|
||||
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(AWSProfile), config.WithDefaultRegion("us-east-1"), config.WithRetryer(
|
||||
func() aws.Retryer {
|
||||
return retry.AddWithMaxAttempts(retry.NewStandard(), 3)
|
||||
}))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = cfg.Credentials.Retrieve(context.TODO())
|
||||
if err != nil {
|
||||
fmt.Printf("[%s] Error retrieving credentials from the specified profile, environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")))
|
||||
log.Fatalf("Could not retrieve the specified profile name %s", err)
|
||||
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func AWSWhoami(awsProfile string) sts.GetCallerIdentityOutput {
|
||||
|
||||
// Connects to STS and checks caller identity. Same as running "aws sts get-caller-identity"
|
||||
//fmt.Printf("[%s] Retrieving caller's identity\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)))
|
||||
|
||||
STSService := sts.NewFromConfig(AWSConfigFileLoader(awsProfile))
|
||||
CallerIdentity, err := STSService.GetCallerIdentity(context.TODO(), &sts.GetCallerIdentityInput{})
|
||||
if err != nil {
|
||||
fmt.Printf("[%s] Could not get caller's identity\n\nError: %s", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), err)
|
||||
log.Fatalf("Could not get caller's identity: %s", err)
|
||||
|
||||
}
|
||||
return *CallerIdentity
|
||||
}
|
||||
|
||||
func GetRegionsForService(awsProfile string, service string) []string {
|
||||
SSMClient := ssm.NewFromConfig(AWSConfigFileLoader(awsProfile))
|
||||
var PaginationControl *string
|
||||
var supportedRegions []string
|
||||
path := fmt.Sprintf("/aws/service/global-infrastructure/services/%s/regions", service)
|
||||
|
||||
ServiceRegions, err := SSMClient.GetParametersByPath(
|
||||
context.TODO(),
|
||||
&(ssm.GetParametersByPathInput{
|
||||
NextToken: PaginationControl,
|
||||
Path: &path,
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
|
||||
}
|
||||
|
||||
if ServiceRegions.Parameters != nil {
|
||||
for _, region := range ServiceRegions.Parameters {
|
||||
name := *region.Value
|
||||
supportedRegions = append(supportedRegions, name)
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ServiceRegions.NextToken != nil {
|
||||
PaginationControl = ServiceRegions.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
}
|
||||
}
|
||||
return supportedRegions
|
||||
}
|
||||
|
||||
// txtLogger - Returns the txt logger
|
||||
func txtLogger() *logrus.Logger {
|
||||
txtLogger := logrus.New()
|
||||
txtFile, err := os.OpenFile(fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath())), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to open log file %v", err))
|
||||
}
|
||||
txtLogger.Out = txtFile
|
||||
txtLogger.SetLevel(logrus.InfoLevel)
|
||||
//txtLogger.SetReportCaller(true)
|
||||
|
||||
return txtLogger
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"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/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
)
|
||||
|
||||
const AzureOutputDirectory = "/azure/"
|
||||
|
||||
func AzNewGraphAuthorizer(tenantID string) autorest.Authorizer {
|
||||
a, err := auth.NewAuthorizerFromCLIWithResource("https://graph.windows.net/")
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Could not create Graph API Client. %s\n", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func AzNewResourceManagerAuthorizer() autorest.Authorizer {
|
||||
a, err := auth.NewAuthorizerFromCLIWithResource("https://management.azure.com/")
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Could not create Azure Resource Manager (ARM) API Client. %s\n", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func AzGetScopeInformation() map[string]map[string][]string {
|
||||
// Auxiliary variables
|
||||
results := make(map[string]map[string][]string)
|
||||
var subscriptionsList []string
|
||||
|
||||
// Clients & Authorizers
|
||||
subscriptionsClient := subscriptions.NewClient()
|
||||
subscriptionsClient.Authorizer = AzNewResourceManagerAuthorizer()
|
||||
|
||||
for page, err := subscriptionsClient.List(context.TODO()); page.NotDone(); err = page.Next() {
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Could not enumerate tenants for current user. %s", err)
|
||||
}
|
||||
// Little bit of an obscure logic here but it works. It's a map of maps.
|
||||
// Format: map[TenantID]map[SubscriptionID][]ResourceGroups
|
||||
// Resource groups are mapped to subscription IDs, which in turn are mapped to tenant IDs.
|
||||
for _, sub := range page.Values() {
|
||||
tenantID := ptr.ToString(sub.TenantID)
|
||||
subscriptionsList = append(subscriptionsList, ptr.ToString(sub.SubscriptionID))
|
||||
|
||||
for _, subID := range subscriptionsList {
|
||||
if _, ok := results[tenantID][subID]; !ok {
|
||||
resourceGroupList := AzGetResourceGroups(subID, subscriptionsClient.Authorizer)
|
||||
if results[tenantID] == nil {
|
||||
results[tenantID] = make(map[string][]string)
|
||||
}
|
||||
results[tenantID][subID] = resourceGroupList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func AzGetResourceGroups(subscriptionID string, authorizer autorest.Authorizer) []string {
|
||||
groupsClient := resources.NewGroupsClient(subscriptionID)
|
||||
groupsClient.Authorizer = authorizer
|
||||
var resourceGroups []string
|
||||
|
||||
for page, err := groupsClient.List(context.TODO(), "", nil); page.NotDone(); err = page.Next() {
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Could not list resource groups for subscription %s. %s", subscriptionID, err)
|
||||
}
|
||||
|
||||
for _, rg := range page.Values() {
|
||||
if rg.Name != nil {
|
||||
resourceGroups = append(resourceGroups, *(rg.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
return resourceGroups
|
||||
}
|
||||
|
||||
/* Azure Public API Edpoints:
|
||||
To request a token: az account get-access-token -o json --resource URL
|
||||
ManagementPortalURL: "https://manage.windowsazure.com/"
|
||||
PublishSettingsURL: "https://manage.windowsazure.com/publishsettings/index"
|
||||
ServiceManagementEndpoint: "https://management.core.windows.net/"
|
||||
ResourceManagerEndpoint: "https://management.azure.com/"
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/"
|
||||
GalleryEndpoint: "https://gallery.azure.com/"
|
||||
KeyVaultEndpoint: "https://vault.azure.net/"
|
||||
GraphEndpoint: "https://graph.windows.net/"
|
||||
ServiceBusEndpoint: "https://servicebus.windows.net/"
|
||||
BatchManagementEndpoint: "https://batch.core.windows.net/"
|
||||
StorageEndpointSuffix: "core.windows.net"
|
||||
CosmosDBDNSSuffix: "documents.azure.com"
|
||||
MariaDBDNSSuffix: "mariadb.database.azure.com"
|
||||
MySQLDatabaseDNSSuffix: "mysql.database.azure.com"
|
||||
PostgresqlDatabaseDNSSuffix: "postgres.database.azure.com"
|
||||
SQLDatabaseDNSSuffix: "database.windows.net"
|
||||
TrafficManagerDNSSuffix: "trafficmanager.net"
|
||||
KeyVaultDNSSuffix: "vault.azure.net"
|
||||
ServiceBusEndpointSuffix: "servicebus.windows.net"
|
||||
ServiceManagementVMDNSSuffix: "cloudapp.net"
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.azure.com"
|
||||
ContainerRegistryDNSSuffix: "azurecr.io"
|
||||
TokenAudience: "https://management.azure.com/"
|
||||
APIManagementHostNameSuffix: "azure-api.net"
|
||||
SynapseEndpointSuffix: "dev.azuresynapse.net"
|
||||
ResourceIdentifiers: github.com/Azure/go-autorest/autorest/azure.ResourceIdentifier {Graph: "https://graph.windows.net/"
|
||||
KeyVault: "https://vault.azure.net"
|
||||
Datalake: "https://datalake.azure.net/"
|
||||
Batch: "https://batch.core.windows.net/"
|
||||
OperationalInsights: "https://api.loganalytics.io"
|
||||
OSSRDBMS: "https://ossrdbms-aad.database.windows.net"
|
||||
Storage: "https://storage.azure.com/"
|
||||
Synapse: "https://dev.azuresynapse.net"
|
||||
ServiceBus: "https://servicebus.azure.net/"
|
||||
SQLDatabase: "https://database.windows.net/"
|
||||
CosmosDB: "https://cosmos.azure.com" */
|
||||
@@ -1,50 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/jedib0t/go-pretty/text"
|
||||
)
|
||||
|
||||
const CloudFoxClientDirName = ".cloudfox"
|
||||
const LogFileName = "cloudfox.log"
|
||||
|
||||
func init() {
|
||||
text.EnableColors()
|
||||
}
|
||||
|
||||
// Initializes logging.
|
||||
// Don't forget to use 'defer file.close()' after invoking this function.
|
||||
func InitLogging() *os.File {
|
||||
// Use the commented line instead to print the file path of the called function
|
||||
// log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.SetFlags(log.LstdFlags)
|
||||
appDir := GetLogDirPath()
|
||||
logFile, err := os.OpenFile(path.Join(ptr.ToString(appDir), LogFileName), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Error opening log file: %s", err)
|
||||
}
|
||||
multiOutput := io.MultiWriter(os.Stdout, logFile)
|
||||
log.SetOutput(multiOutput)
|
||||
return logFile
|
||||
}
|
||||
|
||||
// This function returns ~/.cloudfox.
|
||||
// If the folder does not exist the function creates it.
|
||||
func GetLogDirPath() *string {
|
||||
user, _ := user.Current()
|
||||
dir := filepath.Join(user.HomeDir, CloudFoxClientDirName)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to read or create cloudfox directory")
|
||||
}
|
||||
}
|
||||
return ptr.String(dir)
|
||||
}
|
||||
Reference in new issue
Block a user