diff --git a/.github/workflows/autorelease.yml b/.github/workflows/autorelease.yml index 0844632..df62f45 100644 --- a/.github/workflows/autorelease.yml +++ b/.github/workflows/autorelease.yml @@ -15,8 +15,8 @@ jobs: steps: - - name: Go 1.20 - uses: actions/setup-go@v2 + - name: Go 1.21 + uses: actions/setup-go@v4 with: go-version: ^1.20 id: go @@ -28,7 +28,7 @@ jobs: gpg --list-secret-keys --keyid-format LONG - name: Check Out Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Git Fetch Tags run: git fetch --prune --unshallow --tags -f diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000..92ba67e --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,16 @@ +# This Action uses minimal steps to run in ~5 seconds to rapidly: +# Looks for typos in the codebase using codespell +# https://github.com/codespell-project/codespell#readme +name: codespell +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pip install --user codespell + - run: codespell --ignore-words-list="aks" # --skip="*.css,*.js,*.lock,*.po" diff --git a/.gitignore b/.gitignore index 86f47c6..08bf462 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,5 @@ cloudfox *.txt *.json *.csv -*.log \ No newline at end of file +*.log +dist/ \ No newline at end of file diff --git a/README.md b/README.md index 266067f..21bf608 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,12 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments. ## Demos, Examples, Walkthroughs * [Blog - Introducing: CloudFox](https://bishopfox.com/blog/introducing-cloudfox) +* [Video - Penetration Testing with CloudFox](https://www.youtube.com/watch?v=Ljt_JUp5HbM) * [Video - CloudFox Intro Demos](https://www.youtube.com/watch?v=ReWoUgpUuiQ) -* [Video - Tool Talk: CloudFox AWS sub-command walkthroughs](https://youtu.be/KKsYfL5uVU4?t=360) +* [Video - Tool Talk: CloudFox AWS sub-command walkthroughs](https://youtu.be/KKsYfL5uVU4?t=360) + +## Intentionally Vulnerable Playground +* [CloudFoxable - A Gamified Cloud Hacking Sandbox](https://cloudfoxable.bishopfox.com/) ## Quick Start CloudFox is modular (you can run one command at a time), but there is an `aws all-checks` command that will run the other aws commands for you with sane defaults: @@ -49,11 +53,18 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF **Option 1:** Download the [latest binary release](https://github.com/BishopFox/cloudfox/releases) for your platform. -**Option 2:** [Install Go](https://golang.org/doc/install), clone the CloudFox repository and compile from source +**Option 2:** If you use homebrew: `brew install cloudfox` + +**Option 3:** [Install Go](https://golang.org/doc/install), use `go install github.com/BishopFox/cloudfox@latest` to install from the remote source + +**Option 4:** Developer mode: + + [Install Go](https://golang.org/doc/install), clone the CloudFox repository and compile from source ``` # git clone https://github.com/BishopFox/cloudfox.git ...omitted for brevity... # cd ./cloudfox + # Make any changes necessary # go build . # ./cloudfox ``` @@ -64,7 +75,7 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF ### AWS * AWS CLI installed * Supports AWS profiles, AWS environment variables, or metadata retrieval (on an ec2 instance) - * To run commands on multiple profiles at once, you can specify the path to a file with a list of profile names seperated by a new line using the `-l` flag or pass all stored profiles with the `-a` flag. + * To run commands on multiple profiles at once, you can specify the path to a file with a list of profile names separated by a new line using the `-l` flag or pass all stored profiles with the `-a` flag. * A principal with one recommended policies attached (described below) * Recommended attached policies: **`SecurityAudit` + [CloudFox custom policy](./misc/aws/cloudfox-policy.json)** @@ -138,7 +149,7 @@ CloudFox doesn't create any alerts or findings, and doesn't check your environme **Why do I see errors in some CloudFox commands?** -* Services that don't exist in all regions - CloudFox tries a few ways to figure out what services are supported in each region. However some services don't support the methods CloudFox uses, so CloudFox defaults to just asking every region about the service. Regions that don't suppor the service will return errors. +* Services that don't exist in all regions - CloudFox tries a few ways to figure out what services are supported in each region. However some services don't support the methods CloudFox uses, so CloudFox defaults to just asking every region about the service. Regions that don't support the service will return errors. * You don't have permission - Another reason you might see errors if you don't have permissions to make calls that CloudFox is making. Either because the policy doesn't allow it (e.g., SecurityAudit doesn't allow all of the permissions CloudFox needs. Or, it might be an SCP that is blocking you. You can always look in the ~/.cloudfox/cloudfox-error.log file to get more information on errors. diff --git a/aws/access-keys.go b/aws/access-keys.go index 6928b6f..b2480ec 100644 --- a/aws/access-keys.go +++ b/aws/access-keys.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" @@ -18,9 +19,10 @@ type AccessKeysModule struct { IAMClient sdk.AWSIAMClientInterface Caller sts.GetCallerIdentityOutput AWSProfile string - OutputFormat string Goroutines int WrapTable bool + AWSOutputType string + AWSTableCols string CommandCounter internal.CommandCounter // Main module data @@ -36,8 +38,8 @@ type UserKeys struct { Key string } -func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *AccessKeysModule) PrintAccessKeys(filter string, outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "access-keys" @@ -55,6 +57,7 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o // Variables used to draw table output m.output.Headers = []string{ + "Account", "User Name", "Access Key ID", } @@ -65,6 +68,7 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), key.Username, key.Key, }, @@ -79,9 +83,6 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o //fmt.Printf("[%s][%s] Preparing output.\n\n") m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -89,10 +90,37 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o Wrap: m.WrapTable, }, } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "User Name", + "Access Key ID", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "User Name", + "Access Key ID", + } + } + o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/api-gws.go b/aws/api-gws.go index f19f587..27c09ad 100644 --- a/aws/api-gws.go +++ b/aws/api-gws.go @@ -22,19 +22,18 @@ import ( "github.com/sirupsen/logrus" ) -var CURL_COMMAND string = "curl -k -X %s %s" +var CURL_COMMAND string = "curl -X %s %s" type ApiGwModule struct { // General configuration data APIGatewayClient *apigateway.Client APIGatewayv2Client *apigatewayv2.Client - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + Goroutines int + AWSProfile string + WrapTable bool // Main module data Gateways []ApiGateway @@ -55,7 +54,7 @@ type ApiGateway struct { Method string } -func (m *ApiGwModule) PrintApiGws(outputFormat string, outputDirectory string, verbosity int) { +func (m *ApiGwModule) PrintApiGws(outputDirectory string, verbosity int) { // These stuct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory @@ -131,10 +130,7 @@ func (m *ApiGwModule) PrintApiGws(outputFormat string, outputDirectory string, v } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -218,12 +214,25 @@ func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) { var out string for _, endpoint := range m.Gateways { - line := fmt.Sprintf(CURL_COMMAND, endpoint.Method, endpoint.Endpoint) + method := endpoint.Method + // Write a GET and POST for ANY + if endpoint.Method == "ANY" { + line := fmt.Sprintf(CURL_COMMAND, "GET", endpoint.Endpoint) + if endpoint.ApiKey != "" { + line += fmt.Sprintf(" -H 'X-Api-Key: %s'", endpoint.ApiKey) + } + + out += line + "\n" + + method = "POST" + } + + line := fmt.Sprintf(CURL_COMMAND, method, endpoint.Endpoint) if endpoint.ApiKey != "" { line += fmt.Sprintf(" -H 'X-Api-Key: %s'", endpoint.ApiKey) } - if endpoint.Method == "DELETE" || endpoint.Method == "PATCH" || endpoint.Method == "POST" || endpoint.Method == "PUT" { + if method == "DELETE" || method == "PATCH" || method == "POST" || method == "PUT" { line += " -H 'Content-Type: application/json' -d '{}'" } @@ -239,7 +248,7 @@ func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) { if verbosity > 2 { fmt.Println() - fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed this endpoints into nmap and something like gowitness/aquatone for screenshots.")) + fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Send these requests through your favorite interception proxy")) fmt.Print(out) fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file.")) } diff --git a/aws/buckets.go b/aws/buckets.go index 6c05d33..0334c69 100644 --- a/aws/buckets.go +++ b/aws/buckets.go @@ -20,24 +20,26 @@ import ( type BucketsModule struct { // General configuration data //BucketsS3Client CloudFoxS3Client - S3Client sdk.AWSS3ClientInterface - AWSRegions []string - AWSProfile string - Caller sts.GetCallerIdentityOutput + CheckBucketPolicies bool + S3Client sdk.AWSS3ClientInterface + AWSRegions []string + AWSProfile string + Caller sts.GetCallerIdentityOutput + AWSTableCols string + AWSOutputType string - OutputFormat string - Goroutines int - WrapTable bool + Goroutines int + WrapTable bool // Main module data - Buckets []Bucket + Buckets []BucketRow CommandCounter internal.CommandCounter // Used to store output data for pretty printing output internal.OutputData2 modLog *logrus.Entry } -type Bucket struct { +type BucketRow struct { Arn string AWSService string Region string @@ -53,8 +55,8 @@ type Bucket struct { ResourcePolicySummary string } -func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "buckets" @@ -78,7 +80,7 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks") //create a channel to receive the objects - dataReceiver := make(chan Bucket) + dataReceiver := make(chan BucketRow) // Create a channel to signal to stop receiverDone := make(chan bool) @@ -100,6 +102,7 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ + "Account", "Name", "Region", "Public?", @@ -111,17 +114,16 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Buckets[i].Name, m.Buckets[i].Region, m.Buckets[i].IsPublic, m.Buckets[i].ResourcePolicySummary, }, ) - } + if len(m.output.Body) > 0 { - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -129,10 +131,47 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string Wrap: m.WrapTable, }, } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Name", + "Region", + "Public?", + "Resource Policy Summary", + } + // Otherwise, use the default columns for this module (brief) + } else { + tableCols = []string{ + "Name", + "Region", + "Public?", + "Resource Policy Summary", + } + } + + // Remove the Public? and Resource Policy Summary columns if the user did not specify CheckBucketPolicies + if !m.CheckBucketPolicies { + tableCols = removeStringFromSlice(tableCols, "Public?") + tableCols = removeStringFromSlice(tableCols, "Resource Policy Summary") + } + o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + TableCols: tableCols, + Body: m.output.Body, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -148,7 +187,7 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule) } -func (m *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) { +func (m *BucketsModule) Receiver(receiver chan BucketRow, receiverDone chan bool) { defer close(receiverDone) for { select { @@ -161,7 +200,7 @@ func (m *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) { } } -func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) { +func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) { defer wg.Done() m.CommandCounter.Total++ @@ -215,7 +254,7 @@ func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile } -func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Bucket) { +func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) { defer func() { m.CommandCounter.Executing-- m.CommandCounter.Complete++ @@ -237,42 +276,49 @@ func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, sem } for _, b := range ListBuckets { - bucket := &Bucket{ + bucket := &BucketRow{ Name: aws.ToString(b.Name), AWSService: "S3", } region, err = sdk.CachedGetBucketLocation(m.S3Client, aws.ToString(m.Caller.Account), aws.ToString(b.Name)) if err != nil { m.modLog.Error(err.Error()) - } - bucket.Region = region + region = "Unknown" - policyJSON, err := sdk.CachedGetBucketPolicy(m.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name)) - if err != nil { - m.modLog.Error(err.Error()) - } else { - bucket.PolicyJSON = policyJSON } + bucket.Region = region - policy, err := policy.ParseJSONPolicy([]byte(policyJSON)) - if err != nil { - m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err)) + if m.CheckBucketPolicies { + + policyJSON, err := sdk.CachedGetBucketPolicy(m.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name)) + if err != nil { + m.modLog.Error(err.Error()) + } else { + bucket.PolicyJSON = policyJSON + } + + policy, err := policy.ParseJSONPolicy([]byte(policyJSON)) + if err != nil { + m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err)) + } else { + bucket.Policy = policy + } + + bucket.IsPublic = "No" + if !bucket.Policy.IsEmpty() { + m.analyseBucketPolicy(bucket, dataReceiver) + } else { + bucket.Access = "No resource policy" + dataReceiver <- *bucket + } } else { - bucket.Policy = policy - } - bucket.IsPublic = "No" - if !bucket.Policy.IsEmpty() { - m.analyseBucketPolicy(bucket, dataReceiver) - } else { - bucket.Access = "No resource policy" + // Send Bucket object through the channel to the receiver + bucket.Access = "Skipped" + bucket.IsPublic = "Skipped" dataReceiver <- *bucket } - - // Send Bucket object through the channel to the receiver - } - } func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool { @@ -284,7 +330,7 @@ func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool } -func (m *BucketsModule) analyseBucketPolicy(bucket *Bucket, dataReceiver chan Bucket) { +func (m *BucketsModule) analyseBucketPolicy(bucket *BucketRow, dataReceiver chan BucketRow) { m.storeAccessPolicy(bucket) if bucket.Policy.IsPublic() && !bucket.Policy.IsConditionallyPublic() && !m.isPublicAccessBlocked(bucket.Name, bucket.Region) { @@ -299,14 +345,14 @@ func (m *BucketsModule) analyseBucketPolicy(bucket *Bucket, dataReceiver chan Bu } else { bucket.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account) } - bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n") + //bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n") } dataReceiver <- *bucket } -func (m *BucketsModule) storeAccessPolicy(bucket *Bucket) { +func (m *BucketsModule) storeAccessPolicy(bucket *BucketRow) { f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", bucket.Name)) if err := m.storeFile(f, bucket.PolicyJSON); err != nil { diff --git a/aws/buckets_test.go b/aws/buckets_test.go index 1396ca5..0a93241 100644 --- a/aws/buckets_test.go +++ b/aws/buckets_test.go @@ -81,9 +81,11 @@ func TestListBuckets(t *testing.T) { Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"), Account: aws.String("123456789012"), }, - AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"}, - AWSProfile: "unittesting", - Goroutines: 3, + AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"}, + AWSProfile: "unittesting", + AWSOutputType: "", + CheckBucketPolicies: true, + Goroutines: 3, } subtests := []struct { @@ -91,14 +93,14 @@ func TestListBuckets(t *testing.T) { outputDirectory string verbosity int testModule BucketsModule - expectedResult []Bucket + expectedResult []BucketRow }{ { name: "test1", outputDirectory: ".", verbosity: 2, testModule: m, - expectedResult: []Bucket{{ + expectedResult: []BucketRow{{ Name: "mockBucket123", }}, }, @@ -110,9 +112,9 @@ func TestListBuckets(t *testing.T) { for _, subtest := range subtests { t.Run(subtest.name, func(t *testing.T) { - subtest.testModule.PrintBuckets(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity) + subtest.testModule.PrintBuckets(subtest.outputDirectory, subtest.verbosity) for index, expectedBucket := range subtest.expectedResult { - resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/buckets.txt") + resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/buckets.txt") resultsFile, err := afero.ReadFile(fs, resultsFilePath) if err != nil { t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err) diff --git a/aws/client-initializers.go b/aws/client-initializers.go index f5b07ff..0256c86 100644 --- a/aws/client-initializers.go +++ b/aws/client-initializers.go @@ -7,9 +7,11 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/fsx" + "github.com/aws/aws-sdk-go-v2/service/glue" "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/organizations" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/sts" @@ -124,3 +126,13 @@ func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVers } return orgClient } + +func InitSecretsManagerClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) *secretsmanager.Client { + var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) + return secretsmanager.NewFromConfig(AWSConfig) +} + +func InitGlueClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) *glue.Client { + var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) + return glue.NewFromConfig(AWSConfig) +} diff --git a/aws/cloudformation.go b/aws/cloudformation.go index b2be8bb..959f016 100644 --- a/aws/cloudformation.go +++ b/aws/cloudformation.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" @@ -20,12 +21,13 @@ type CloudformationModule struct { // General configuration data CloudFormationClient sdk.CloudFormationClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + Goroutines int + AWSProfile string + WrapTable bool + AWSOutputType string + AWSTableCols string // Main module data CFStacks []CFStack @@ -45,8 +47,8 @@ type CFStack struct { Template string } -func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "cloudformation" @@ -93,48 +95,45 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, ou // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ + "Account", "Service", "Region", "Name", "Role", - // "Parameters", - // "Outputs", + "Parameters", + "Outputs", } // Table rows for i := range m.CFStacks { - // var isParameters string - // var isOutputs string - // if m.CFStacks[i].Parameters != nil { - // isParameters = "Y" - // } else { - // isParameters = "N" - // } - // if m.CFStacks[i].Outputs != nil { - // isOutputs = "Y" - // } else { - // isOutputs = "N" - // } + var hasParameters string + var hasOutputs string + if m.CFStacks[i].Parameters != nil { + hasParameters = "Y" + } else { + hasParameters = "N" + } + if m.CFStacks[i].Outputs != nil { + hasOutputs = "Y" + } else { + hasOutputs = "N" + } m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.CFStacks[i].AWSService, m.CFStacks[i].Region, m.CFStacks[i].Name, m.CFStacks[i].Role, - // isParameters, - // isOutputs, + hasParameters, + hasOutputs, }, ) } if len(m.output.Body) > 0 { - // m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile)) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -142,10 +141,43 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, ou Wrap: m.WrapTable, }, } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Service", + "Region", + "Name", + "Role", + "Parameters", + "Outputs", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Service", + "Region", + "Name", + "Role", + } + } + o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/cloudformation_test.go b/aws/cloudformation_test.go index 7b78c1f..714c3fa 100644 --- a/aws/cloudformation_test.go +++ b/aws/cloudformation_test.go @@ -1,81 +1,15 @@ package aws import ( - "context" - "encoding/json" "log" "testing" - "time" + "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/cloudformation" - "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" "github.com/aws/aws-sdk-go-v2/service/sts" ) -const DESCRIBE_STACKS_TEST_FILE = "./test-data/cloudformation-describestacks.json" -const TEMPLATE_BODY_TEST_FILE = "./test-data/cloudformation-getTemplate.json" - -type Stacks struct { - Stacks []struct { - StackID string `json:"StackId"` - Description string `json:"Description"` - Tags []interface{} `json:"Tags"` - Outputs []types.Output `json:"Outputs"` - Parameters []types.Parameter `json:"Parameters"` - StackStatusReason interface{} `json:"StackStatusReason"` - CreationTime time.Time `json:"CreationTime"` - Capabilities []interface{} `json:"Capabilities"` - StackName string `json:"StackName"` - RoleArn string `json:"RoleArn"` - StackStatus string `json:"StackStatus"` - DisableRollback bool `json:"DisableRollback"` - } `json:"Stacks"` -} - -type TemplateBody struct { - TemplateBody string `json:"TemplateBody"` -} - -type MockedCloudformationClient struct { - describeStacks Stacks - getTemplateBody TemplateBody -} - -func (m *MockedCloudformationClient) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) { - - err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.describeStacks) - if err != nil { - log.Fatalf("can't unmarshall file %s", DESCRIBE_STACKS_TEST_FILE) - } - var stacks []types.Stack - for _, stack := range m.describeStacks.Stacks { - stacks = append(stacks, types.Stack{ - StackName: &stack.StackName, - RoleARN: &stack.RoleArn, - Outputs: stack.Outputs, - Parameters: stack.Parameters, - }) - - } - - return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil -} - -func (m *MockedCloudformationClient) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) { - err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.getTemplateBody) - if err != nil { - log.Fatalf("can't unmarshall file %s", TEMPLATE_BODY_TEST_FILE) - } - - return &cloudformation.GetTemplateOutput{TemplateBody: &m.getTemplateBody.TemplateBody}, nil -} - -func (m *MockedCloudformationClient) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) { - return &cloudformation.ListStacksOutput{}, nil -} - func TestCloudFormation(t *testing.T) { subtests := []struct { name string @@ -89,9 +23,8 @@ func TestCloudFormation(t *testing.T) { outputDirectory: ".", verbosity: 2, testModule: CloudformationModule{ - CloudFormationClient: &MockedCloudformationClient{}, + CloudFormationClient: &sdk.MockedCloudformationClient{}, Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")}, - OutputFormat: "table", AWSProfile: "test", Goroutines: 30, AWSRegions: AWSRegions, @@ -105,7 +38,7 @@ func TestCloudFormation(t *testing.T) { internal.MockFileSystem(true) for _, subtest := range subtests { t.Run(subtest.name, func(t *testing.T) { - subtest.testModule.PrintCloudformationStacks(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity) + subtest.testModule.PrintCloudformationStacks(subtest.outputDirectory, subtest.verbosity) for index, expectedStack := range subtest.expectedResult { if expectedStack.Name != subtest.testModule.CFStacks[index].Name { log.Fatal("Stack name does not match expected name") diff --git a/aws/codebuild.go b/aws/codebuild.go index 46a8d08..5bb7697 100644 --- a/aws/codebuild.go +++ b/aws/codebuild.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "path/filepath" + "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" @@ -18,9 +19,11 @@ type CodeBuildModule struct { CodeBuildClient sdk.CodeBuildClientInterface IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + Goroutines int AWSProfile string SkipAdminCheck bool @@ -46,8 +49,8 @@ type Project struct { CanPrivEsc string } -func (m *CodeBuildModule) PrintCodeBuildProjects(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "codebuild" @@ -107,50 +110,66 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputFormat string, outputDire } // add - if struct is not empty do this. otherwise, dont write anything. - if m.pmapperError == nil { - m.output.Headers = []string{ + m.output.Headers = []string{ + "Account", + "Region", + "Name", + "Role", + "IsAdminRole?", + "CanPrivEscToAdmin?", + } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", "Region", "Name", "Role", "IsAdminRole?", "CanPrivEscToAdmin?", } + // Otherwise, use the default columns. } else { - m.output.Headers = []string{ + tableCols = []string{ "Region", "Name", "Role", "IsAdminRole?", - //"CanPrivEscToAdmin?", + "CanPrivEscToAdmin?", } } + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") + } + // Table rows for i := range m.Projects { - if m.pmapperError == nil { - m.output.Body = append( - m.output.Body, - []string{ - m.Projects[i].Region, - m.Projects[i].Name, - m.Projects[i].Role, - m.Projects[i].Admin, - m.Projects[i].CanPrivEsc, - }, - ) - } else { - m.output.Body = append( - m.output.Body, - []string{ - m.Projects[i].Region, - m.Projects[i].Name, - m.Projects[i].Role, - m.Projects[i].Admin, - }, - ) - - } + m.output.Body = append( + m.output.Body, + []string{ + aws.ToString(m.Caller.Account), + m.Projects[i].Region, + m.Projects[i].Name, + m.Projects[i].Role, + m.Projects[i].Admin, + m.Projects[i].CanPrivEsc, + }, + ) } var seen []string @@ -162,11 +181,6 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputFormat string, outputDire if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) - //fmt.Printf("[%s][%s] %d projects with a total of %d node groups found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(seen), len(m.output.Body)) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -175,9 +189,10 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputFormat string, outputDire }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/codebuild_test.go b/aws/codebuild_test.go index dbd825b..51cd1c4 100644 --- a/aws/codebuild_test.go +++ b/aws/codebuild_test.go @@ -124,9 +124,9 @@ func TestCodeBuildProjects(t *testing.T) { tmpDir := "." // execute the module with verbosity set to 2 - m.PrintCodeBuildProjects("table", tmpDir, 2) + m.PrintCodeBuildProjects(tmpDir, 2) - resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/codebuild.txt") + resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/codebuild.txt") resultsFile, err := afero.ReadFile(fs, resultsFilePath) if err != nil { t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err) diff --git a/aws/databases.go b/aws/databases.go index 8008e85..3582506 100644 --- a/aws/databases.go +++ b/aws/databases.go @@ -7,6 +7,7 @@ import ( "path/filepath" "sort" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" @@ -19,16 +20,18 @@ import ( type DatabasesModule struct { RDSClient sdk.RDSClientInterface - RedshiftClient sdk.RedShiftClientInterface + RedshiftClient sdk.AWSRedShiftClientInterface DynamoDBClient sdk.DynamoDBClientInterface DocDBClient sdk.DocDBClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool Databases []Database CommandCounter internal.CommandCounter @@ -52,8 +55,8 @@ type Database struct { Size string } -func (m *DatabasesModule) PrintDatabases(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "databases" @@ -101,6 +104,7 @@ func (m *DatabasesModule) PrintDatabases(outputFormat string, outputDirectory st }) m.output.Headers = []string{ + "Account", "Service", "Engine", "Region", @@ -114,11 +118,47 @@ func (m *DatabasesModule) PrintDatabases(outputFormat string, outputDirectory st } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Service", + "Engine", + "Region", + "Name", + "Size", + "UserName", + "Endpoint", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Service", + "Engine", + "Region", + "Name", + "Size", + "UserName", + "Endpoint", + } + } + // Table rows for i := range m.Databases { m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Databases[i].AWSService, m.Databases[i].Engine, m.Databases[i].Region, @@ -135,10 +175,7 @@ func (m *DatabasesModule) PrintDatabases(outputFormat string, outputDirectory st } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -147,9 +184,10 @@ func (m *DatabasesModule) PrintDatabases(outputFormat string, outputDirectory st }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -287,7 +325,7 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, endpoint := aws.ToString(instance.Endpoint.Address) engine := aws.ToString(instance.Engine) - if instance.PubliclyAccessible { + if aws.ToBool(instance.PubliclyAccessible) { public = "True" } else { public = "False" @@ -300,7 +338,7 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, Engine: engine, Endpoint: endpoint, UserName: aws.ToString(instance.MasterUsername), - Port: port, + Port: aws.ToInt32(port), Protocol: aws.ToString(instance.Engine), Public: public, } @@ -344,7 +382,7 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG //id := workspace.Id endpoint := aws.ToString(cluster.Endpoint.Address) - if cluster.PubliclyAccessible { + if aws.ToBool(cluster.PubliclyAccessible) { public = "True" } else { public = "False" @@ -355,7 +393,7 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG Region: r, Name: name, Endpoint: endpoint, - Port: port, + Port: aws.ToInt32(port), Protocol: protocol, Public: public, } diff --git a/aws/ecr.go b/aws/ecr.go index 0406fe4..46933fa 100644 --- a/aws/ecr.go +++ b/aws/ecr.go @@ -21,13 +21,15 @@ import ( type ECRModule struct { // General configuration data - ECRClient sdk.AWSECRClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + ECRClient sdk.AWSECRClientInterface + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data Repositories []Repository @@ -49,8 +51,8 @@ type Repository struct { PolicyJSON string } -func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "ecr" @@ -95,8 +97,9 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos receiverDone <- true <-receiverDone - // add - if struct is not empty do this. otherwise, dont write anything. + // This is the complete list of potential table columns m.output.Headers = []string{ + "Account", "Service", "Region", "Name", @@ -106,11 +109,53 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos "ImageSize", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Service", + "Region", + "Name", + "URI", + "PushedAt", + "ImageTags", + "ImageSize", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Service", + "Region", + "Name", + "URI", + "PushedAt", + "ImageTags", + "ImageSize", + } + } + + // sort the table by Name + sort.Slice(m.Repositories, func(i, j int) bool { + return m.Repositories[i].Name < m.Repositories[j].Name + }) + // Table rows for i := range m.Repositories { m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Repositories[i].AWSService, m.Repositories[i].Region, m.Repositories[i].Name, @@ -124,10 +169,7 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -136,9 +178,10 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -239,92 +282,89 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho <-semaphore }() - var allImages []types.ImageDetail - var repoURI string - var repoName string - - DescribeRepositories, err := m.describeRepositories(r) + Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } - for _, repo := range DescribeRepositories { - repoName = aws.ToString(repo.RepositoryName) - repoURI = aws.ToString(repo.RepositoryUri) + for _, repo := range Repositories { + repoName := aws.ToString(repo.RepositoryName) + repoURI := aws.ToString(repo.RepositoryUri) //created := *repo.CreatedAt //fmt.Printf("%s, %s, %s", repoName, repoURI, created) - - images, err := m.describeImages(r, repoName) + images, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ - return + continue } - allImages = append(allImages, images...) - } - sort.Slice(allImages, func(i, j int) bool { - return allImages[i].ImagePushedAt.Format("2006-01-02 15:04:05") < allImages[j].ImagePushedAt.Format("2006-01-02 15:04:05") - }) + sort.Slice(images, func(i, j int) bool { + return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05") + }) - var image types.ImageDetail - var imageTags string + var image types.ImageDetail + var imageTags string - if len(allImages) > 1 { - image = allImages[len(allImages)-1] - } else if len(allImages) == 1 { - image = allImages[0] - } else { - return - } + if len(images) > 1 { + image = images[len(images)-1] + } else if len(images) == 1 { + image = images[0] + } else { + continue + } - if len(image.ImageTags) > 0 { - imageTags = image.ImageTags[0] - } - //imageTags := image.ImageTags[0] - pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05") - imageSize := aws.ToInt64(image.ImageSizeInBytes) - pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags) - - dataReceiver <- Repository{ - AWSService: "ECR", - Name: repoName, - Region: r, - URI: pullURI, - PushedAt: pushedAt, - ImageTags: imageTags, - ImageSize: imageSize, + if len(image.ImageTags) > 0 { + imageTags = image.ImageTags[0] + } else { + imageTags = "No tags" + } + + //imageTags := image.ImageTags[0] + pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05") + imageSize := aws.ToInt64(image.ImageSizeInBytes) + pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags) + + dataReceiver <- Repository{ + AWSService: "ECR", + Name: repoName, + Region: r, + URI: pullURI, + PushedAt: pushedAt, + ImageTags: imageTags, + ImageSize: imageSize, + } } } -func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) { +// func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) { - var repositories []types.Repository - Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r) - if err != nil { - m.CommandCounter.Error++ - return nil, err - } +// var repositories []types.Repository +// Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r) +// if err != nil { +// m.CommandCounter.Error++ +// return nil, err +// } - repositories = append(repositories, Repositories...) +// repositories = append(repositories, Repositories...) - return repositories, nil -} +// return repositories, nil +// } -func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) { - var images []types.ImageDetail +// func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) { +// var images []types.ImageDetail - ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName) - if err != nil { - m.CommandCounter.Error++ - return nil, err - } - images = append(images, ImageDetails...) - return images, nil -} +// ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName) +// if err != nil { +// m.CommandCounter.Error++ +// return nil, err +// } +// images = append(images, ImageDetails...) +// return images, nil +// } func (m *ECRModule) getECRRepositoryPolicy(r string, repository string) (policy.Policy, error) { var repoPolicy policy.Policy diff --git a/aws/ecr_test.go b/aws/ecr_test.go index 1b498f5..2e791f3 100644 --- a/aws/ecr_test.go +++ b/aws/ecr_test.go @@ -1,52 +1,15 @@ package aws import ( - "context" "log" "testing" - "time" + "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ecr" - "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/aws/aws-sdk-go-v2/service/sts" ) -var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"} - -type MockedECRClient struct { -} - -func (m *MockedECRClient) DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) { - return &ecr.DescribeRepositoriesOutput{ - Repositories: []types.Repository{ - { - RepositoryName: aws.String("test1"), - RepositoryUri: aws.String("testURI"), - }, - }, - }, nil -} - -func (m *MockedECRClient) DescribeImages(context.Context, *ecr.DescribeImagesInput, ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) { - return &ecr.DescribeImagesOutput{ - ImageDetails: []types.ImageDetail{ - { - ImagePushedAt: aws.Time(time.Now()), - ImageSizeInBytes: aws.Int64(123456), - ImageTags: []string{"latest"}, - }, - }, - }, nil -} - -func (m *MockedECRClient) GetRepositoryPolicy(context.Context, *ecr.GetRepositoryPolicyInput, ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) { - return &ecr.GetRepositoryPolicyOutput{ - PolicyText: aws.String("policyText"), - }, nil -} - func TestDescribeRepos(t *testing.T) { subtests := []struct { @@ -61,29 +24,37 @@ func TestDescribeRepos(t *testing.T) { outputDirectory: ".", verbosity: 2, testModule: ECRModule{ - ECRClient: &MockedECRClient{}, + ECRClient: &sdk.MockedECRClient{}, Caller: sts.GetCallerIdentityOutput{ Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"), Account: aws.String("123456789012"), }, - OutputFormat: "table", - AWSProfile: "test", - Goroutines: 30, - AWSRegions: AWSRegions, + AWSProfile: "test", + Goroutines: 30, + AWSRegions: []string{"us-east-1"}, }, - expectedResult: []Repository{{ - Name: "test1", - URI: "testURI:latest", - PushedAt: "2022-10-25 15:14:06", - ImageTags: "latest", - ImageSize: 123456, - }}, + expectedResult: []Repository{ + { + Name: "repo1", + URI: "11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo1", + PushedAt: "2022-10-25 15:14:00", + ImageTags: "customtag, tag2", + ImageSize: 123456, + }, + { + Name: "repo2", + URI: "11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo2", + PushedAt: "2021-10-15 11:14:00", + ImageTags: "latest", + ImageSize: 2222222, + }}, }, } + internal.MockFileSystem(true) for _, subtest := range subtests { t.Run(subtest.name, func(t *testing.T) { - subtest.testModule.PrintECR(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity) + subtest.testModule.PrintECR(subtest.outputDirectory, subtest.verbosity) for index, expectedRepo := range subtest.expectedResult { if expectedRepo.Name != subtest.testModule.Repositories[index].Name { log.Fatal("Repo name does not match expected name") diff --git a/aws/ecs-tasks.go b/aws/ecs-tasks.go index 1197207..f583cf0 100644 --- a/aws/ecs-tasks.go +++ b/aws/ecs-tasks.go @@ -13,7 +13,6 @@ import ( "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/aws/aws-sdk-go-v2/service/sts" @@ -21,20 +20,16 @@ import ( "github.com/sirupsen/logrus" ) -type DescribeTasksDefinitionAPIClient interface { - DescribeTaskDefinition(context.Context, *ecs.DescribeTaskDefinitionInput, ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) -} type ECSTasksModule struct { - DescribeTaskDefinitionClient DescribeTasksDefinitionAPIClient - DescribeTasksClient ecs.DescribeTasksAPIClient - ListTasksClient ecs.ListTasksAPIClient - ListClustersClient ecs.ListClustersAPIClient - DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient - IAMClient sdk.AWSIAMClientInterface - - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string + ECSClient sdk.AWSECSClientInterface + EC2Client sdk.AWSEC2ClientInterface + IAMClient sdk.AWSIAMClientInterface + + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + AWSProfile string Goroutines int SkipAdminCheck bool @@ -63,7 +58,7 @@ type MappedECSTask struct { CanPrivEsc string } -func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, verbosity int) { +func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) { m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "ecs-tasks" @@ -126,7 +121,7 @@ func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, v receiverDone <- true <-receiverDone - m.printECSTaskData(outputFormat, outputDirectory, dataReceiver, verbosity) + m.printECSTaskData(outputDirectory, dataReceiver, verbosity) } @@ -143,9 +138,36 @@ func (m *ECSTasksModule) Receiver(receiver chan MappedECSTask, receiverDone chan } } -func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory string, dataReceiver chan MappedECSTask, verbosity int) { - if m.pmapperError == nil { - m.output.Headers = []string{ +func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver chan MappedECSTask, verbosity int) { + // This is the complete list of potential table columns + m.output.Headers = []string{ + "Account", + "Cluster", + "TaskDefinition", + "LaunchType", + "ID", + "External IP", + "Internal IP", + "RoleArn", + "IsAdminRole?", + "CanPrivEscToAdmin?", + } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", "Cluster", "TaskDefinition", "LaunchType", @@ -156,61 +178,46 @@ func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory s "IsAdminRole?", "CanPrivEscToAdmin?", } + // Otherwise, use the default columns. } else { - m.output.Headers = []string{ + tableCols = []string{ "Cluster", "TaskDefinition", "LaunchType", - "ID", "External IP", "Internal IP", "RoleArn", "IsAdminRole?", - //"CanPrivEscToAdmin?", + "CanPrivEscToAdmin?", } } - if m.pmapperError == nil { - for _, ecsTask := range m.MappedECSTasks { - m.output.Body = append( - m.output.Body, - []string{ - ecsTask.Cluster, - ecsTask.TaskDefinitionName, - ecsTask.LaunchType, - ecsTask.ID, - ecsTask.ExternalIP, - ecsTask.PrivateIP, - ecsTask.Role, - ecsTask.Admin, - ecsTask.CanPrivEsc, - }, - ) - } - } else { - for _, ecsTask := range m.MappedECSTasks { - m.output.Body = append( - m.output.Body, - []string{ - ecsTask.Cluster, - ecsTask.TaskDefinitionName, - ecsTask.LaunchType, - ecsTask.ID, - ecsTask.ExternalIP, - ecsTask.PrivateIP, - ecsTask.Role, - ecsTask.Admin, - //ecsTask.CanPrivEsc, - }, - ) - } + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") + } + + for _, ecsTask := range m.MappedECSTasks { + m.output.Body = append( + m.output.Body, + []string{ + aws.ToString(m.Caller.Account), + ecsTask.Cluster, + ecsTask.TaskDefinitionName, + ecsTask.LaunchType, + ecsTask.ID, + ecsTask.ExternalIP, + ecsTask.PrivateIP, + ecsTask.Role, + ecsTask.Admin, + ecsTask.CanPrivEsc, + }, + ) } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -219,9 +226,10 @@ func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory s }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -313,75 +321,37 @@ func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceive func (m *ECSTasksModule) getListClusters(region string, dataReceiver chan MappedECSTask) { - var PaginationControl *string - for { - ListClusters, err := m.ListClustersClient.ListClusters( - context.TODO(), - &(ecs.ListClustersInput{ - NextToken: PaginationControl, - }), - func(o *ecs.Options) { - o.Region = region - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - - for _, clusterARN := range ListClusters.ClusterArns { - m.getListTasks(clusterARN, region, dataReceiver) - } - - if ListClusters.NextToken != nil { - PaginationControl = ListClusters.NextToken - } else { - PaginationControl = nil - break - } + ClusterArns, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), region) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + for _, clusterARN := range ClusterArns { + m.getListTasks(clusterARN, region, dataReceiver) } + } func (m *ECSTasksModule) getListTasks(clusterARN string, region string, dataReceiver chan MappedECSTask) { - var PaginationControl *string - for { - - ListTasks, err := m.ListTasksClient.ListTasks( - context.TODO(), - &(ecs.ListTasksInput{ - Cluster: aws.String(clusterARN), - NextToken: PaginationControl, - }), - func(o *ecs.Options) { - o.Region = region - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - - batchSize := 100 // maximum value: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTasks.html#API_DescribeTasks_RequestSyntax - for i := 0; i < len(ListTasks.TaskArns); i += batchSize { - j := i + batchSize - if j > len(ListTasks.TaskArns) { - j = len(ListTasks.TaskArns) - } - - m.loadTasksData(clusterARN, ListTasks.TaskArns[i:j], region, dataReceiver) - } + TaskArns, err := sdk.CachedECSListTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - if ListTasks.NextToken != nil { - PaginationControl = ListTasks.NextToken - } else { - PaginationControl = nil - break + batchSize := 100 // maximum value: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTasks.html#API_DescribeTasks_RequestSyntax + for i := 0; i < len(TaskArns); i += batchSize { + j := i + batchSize + if j > len(TaskArns) { + j = len(TaskArns) } + m.loadTasksData(clusterARN, TaskArns[i:j], region, dataReceiver) } + } func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, region string, dataReceiver chan MappedECSTask) { @@ -390,16 +360,7 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg return } - DescribeTasks, err := m.DescribeTasksClient.DescribeTasks( - context.TODO(), - &(ecs.DescribeTasksInput{ - Cluster: aws.String(clusterARN), - Tasks: taskARNs, - }), - func(o *ecs.Options) { - o.Region = region - }, - ) + Tasks, err := sdk.CachedECSDescribeTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN, taskARNs) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -407,7 +368,7 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg } eniIDs := []string{} - for _, task := range DescribeTasks.Tasks { + for _, task := range Tasks { eniID := getElasticNetworkInterfaceIDOfECSTask(task) if eniID != "" { eniIDs = append(eniIDs, eniID) @@ -420,8 +381,9 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg return } - for _, task := range DescribeTasks.Tasks { - taskDefinition, err := m.describeTaskDefinition(aws.ToString(task.TaskDefinitionArn), region) + for _, task := range Tasks { + //taskDefinition, err := m.describeTaskDefinition(aws.ToString(task.TaskDefinitionArn), region) + taskDefinition, err := sdk.CachedECSDescribeTaskDefinition(m.ECSClient, aws.ToString(m.Caller.Account), region, aws.ToString(task.TaskDefinitionArn)) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -461,7 +423,7 @@ func getTaskDefinitionContent(taskDefinition types.TaskDefinition) string { } func (m *ECSTasksModule) describeTaskDefinition(taskDefinitionArn string, region string) (types.TaskDefinition, error) { - DescribeTaskDefinition, err := m.DescribeTaskDefinitionClient.DescribeTaskDefinition( + DescribeTaskDefinition, err := m.ECSClient.DescribeTaskDefinition( context.TODO(), &ecs.DescribeTaskDefinitionInput{ TaskDefinition: &taskDefinitionArn, @@ -511,20 +473,13 @@ func (m *ECSTasksModule) loadPublicIPs(eniIDs []string, region string) (map[stri if len(eniIDs) == 0 { return eniPublicIPs, nil } - DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces( - context.TODO(), - &(ec2.DescribeNetworkInterfacesInput{ - NetworkInterfaceIds: eniIDs, - }), - func(o *ec2.Options) { - o.Region = region - }, - ) + + NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region) if err != nil { return nil, fmt.Errorf("getting elastic network interfaces: %s", err) } - for _, eni := range DescribeNetworkInterfaces.NetworkInterfaces { + for _, eni := range NetworkInterfaces { eniPublicIPs[aws.ToString(eni.NetworkInterfaceId)] = getPublicIPOfElasticNetworkInterface(eni) } diff --git a/aws/ecs-tasks_test.go b/aws/ecs-tasks_test.go index 371d05d..e5b5495 100644 --- a/aws/ecs-tasks_test.go +++ b/aws/ecs-tasks_test.go @@ -1,249 +1,15 @@ package aws import ( - "context" - "encoding/json" "log" - "os" "testing" - "time" + "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ec2" - ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/aws/aws-sdk-go-v2/service/ecs" - ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/aws/aws-sdk-go-v2/service/sts" ) -const DESCRIBE_TASKS_TEST_FILE = "./test-data/describe-tasks.json" -const DESCRIBE_NETWORK_INTEFACES_TEST_FILE = "./test-data/describe-network-interfaces.json" - -type ListTasks struct { - TaskArns []string `json:"taskArns"` -} - -type DescribeTasks struct { - Tasks []struct { - Attachments []struct { - ID string `json:"id"` - Type string `json:"type"` - Status string `json:"status"` - Details []struct { - Name string `json:"name"` - Value string `json:"value"` - } `json:"details"` - } `json:"attachments"` - Attributes []struct { - Name string `json:"name"` - Value string `json:"value"` - } `json:"attributes"` - AvailabilityZone string `json:"availabilityZone"` - ClusterArn string `json:"clusterArn"` - Connectivity string `json:"connectivity"` - ConnectivityAt string `json:"connectivityAt"` - Containers []struct { - ContainerArn string `json:"containerArn"` - TaskArn string `json:"taskArn"` - Name string `json:"name"` - Image string `json:"image"` - RuntimeID string `json:"runtimeId"` - LastStatus string `json:"lastStatus"` - NetworkBindings []interface{} `json:"networkBindings"` - NetworkInterfaces []struct { - AttachmentID string `json:"attachmentId"` - PrivateIpv4Address string `json:"privateIpv4Address"` - } `json:"networkInterfaces"` - HealthStatus string `json:"healthStatus"` - CPU string `json:"cpu"` - Memory string `json:"memory"` - } `json:"containers"` - CPU string `json:"cpu"` - CreatedAt string `json:"createdAt"` - DesiredStatus string `json:"desiredStatus"` - EnableExecuteCommand bool `json:"enableExecuteCommand"` - Group string `json:"group"` - HealthStatus string `json:"healthStatus"` - LastStatus string `json:"lastStatus"` - LaunchType string `json:"launchType"` - Memory string `json:"memory"` - Overrides struct { - ContainerOverrides []struct { - Name string `json:"name"` - } `json:"containerOverrides"` - InferenceAcceleratorOverrides []interface{} `json:"inferenceAcceleratorOverrides"` - } `json:"overrides"` - PlatformVersion string `json:"platformVersion"` - PlatformFamily string `json:"platformFamily"` - PullStartedAt string `json:"pullStartedAt"` - PullStoppedAt string `json:"pullStoppedAt"` - StartedAt string `json:"startedAt"` - StartedBy string `json:"startedBy"` - Tags []interface{} `json:"tags"` - TaskArn string `json:"taskArn"` - TaskDefinitionArn string `json:"taskDefinitionArn"` - Version int `json:"version"` - EphemeralStorage struct { - SizeInGiB int `json:"sizeInGiB"` - } `json:"ephemeralStorage"` - } `json:"tasks"` - Failures []interface{} `json:"failures"` -} - -type DescribeNetworkInterfaces struct { - NetworkInterfaces []struct { - Status string `json:"Status"` - MacAddress string `json:"MacAddress"` - SourceDestCheck bool `json:"SourceDestCheck"` - VpcID string `json:"VpcId"` - Description string `json:"Description"` - Association struct { - PublicIP string `json:"PublicIp"` - AssociationID string `json:"AssociationId"` - PublicDNSName string `json:"PublicDnsName"` - IPOwnerID string `json:"IpOwnerId"` - } `json:"Association"` - NetworkInterfaceID string `json:"NetworkInterfaceId"` - PrivateIPAddresses []struct { - PrivateDNSName string `json:"PrivateDnsName"` - Association struct { - PublicIP string `json:"PublicIp"` - AssociationID string `json:"AssociationId"` - PublicDNSName string `json:"PublicDnsName"` - IPOwnerID string `json:"IpOwnerId"` - } `json:"Association"` - Primary bool `json:"Primary"` - PrivateIPAddress string `json:"PrivateIpAddress"` - } `json:"PrivateIpAddresses"` - RequesterManaged bool `json:"RequesterManaged"` - Ipv6Addresses []interface{} `json:"Ipv6Addresses"` - PrivateDNSName string `json:"PrivateDnsName,omitempty"` - AvailabilityZone string `json:"AvailabilityZone"` - Attachment struct { - Status string `json:"Status"` - DeviceIndex int `json:"DeviceIndex"` - AttachTime time.Time `json:"AttachTime"` - InstanceID string `json:"InstanceId"` - DeleteOnTermination bool `json:"DeleteOnTermination"` - AttachmentID string `json:"AttachmentId"` - InstanceOwnerID string `json:"InstanceOwnerId"` - } `json:"Attachment"` - Groups []struct { - GroupName string `json:"GroupName"` - GroupID string `json:"GroupId"` - } `json:"Groups"` - SubnetID string `json:"SubnetId"` - OwnerID string `json:"OwnerId"` - TagSet []interface{} `json:"TagSet"` - PrivateIPAddress string `json:"PrivateIpAddress"` - } `json:"NetworkInterfaces"` -} - -func readTestFile(testFile string) []byte { - file, err := os.ReadFile(testFile) - if err != nil { - log.Fatalf("can't read file %s", testFile) - } - return file -} - -type mockedListclustersClient struct { -} - -func (c *mockedListclustersClient) ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) { - return &ecs.ListClustersOutput{ClusterArns: []string{ - "arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster", - "arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster2", - "arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster3", - }}, nil -} - -type mockedListTasksClient struct{} - -func (c *mockedListTasksClient) ListTasks(ctx context.Context, input *ecs.ListTasksInput, f ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) { - return &ecs.ListTasksOutput{TaskArns: []string{ - "arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE", - "arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE", - }}, nil -} - -type mockedDescribeTasksClient struct { - describeTasks DescribeTasks -} - -func (c *mockedDescribeTasksClient) DescribeTasks(ctx context.Context, input *ecs.DescribeTasksInput, f ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { - err := json.Unmarshal(readTestFile(DESCRIBE_TASKS_TEST_FILE), &c.describeTasks) - if err != nil { - log.Fatalf("can't unmarshall file %s", DESCRIBE_TASKS_TEST_FILE) - } - var tasks []ecsTypes.Task - for _, mockedTask := range c.describeTasks.Tasks { - if mockedTask.ClusterArn == aws.ToString(input.Cluster) { - for _, inputTask := range input.Tasks { - if mockedTask.TaskArn == inputTask { - var attachments []ecsTypes.Attachment - for _, a := range mockedTask.Attachments { - var deets []ecsTypes.KeyValuePair - for _, detail := range a.Details { - deets = append(deets, ecsTypes.KeyValuePair{ - Name: aws.String(detail.Name), - Value: aws.String(detail.Value)}) - } - attachments = append(attachments, ecsTypes.Attachment{ - Type: aws.String(a.Type), - Details: deets, - Id: aws.String(a.ID), - Status: aws.String(a.Status), - }) - } - tasks = append(tasks, ecsTypes.Task{ - ClusterArn: aws.String(mockedTask.ClusterArn), - TaskDefinitionArn: aws.String(mockedTask.TaskDefinitionArn), - LaunchType: ecsTypes.LaunchType(*aws.String(mockedTask.LaunchType)), - TaskArn: aws.String(mockedTask.TaskArn), - Attachments: attachments, - }) - } - } - } - } - return &ecs.DescribeTasksOutput{Tasks: tasks}, nil -} - -type mockedDescribeNetworkInterfacesClient struct { - describeNetworkInterfaces DescribeNetworkInterfaces -} - -func (c *mockedDescribeNetworkInterfacesClient) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) { - var nics []ec2types.NetworkInterface - err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces) - if err != nil { - log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE) - } - for _, mockedNic := range c.describeNetworkInterfaces.NetworkInterfaces { - for _, inputNicID := range input.NetworkInterfaceIds { - if mockedNic.NetworkInterfaceID == inputNicID { - nics = append(nics, ec2types.NetworkInterface{ - Association: &ec2types.NetworkInterfaceAssociation{ - PublicIp: aws.String(mockedNic.Association.PublicIP), - }, - NetworkInterfaceId: aws.String(mockedNic.NetworkInterfaceID)}) - } - } - } - return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil -} - -type mockedDescribeTaskDefinitionInterface struct { -} - -func (c *mockedDescribeTaskDefinitionInterface) DescribeTaskDefinition(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, f ...func(o *ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { - testTaskDefinition := ecsTypes.TaskDefinition{} - testTaskDefinition.TaskRoleArn = aws.String("test123") - return &ecs.DescribeTaskDefinitionOutput{TaskDefinition: &testTaskDefinition}, nil -} - func TestECSTasks(t *testing.T) { subtests := []struct { name string @@ -258,17 +24,13 @@ func TestECSTasks(t *testing.T) { verbosity: 2, testModule: ECSTasksModule{ - AWSProfile: "default", - AWSRegions: []string{"us-east-1", "us-west-1"}, - Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")}, - SkipAdminCheck: true, - Goroutines: 30, - DescribeNetworkInterfacesClient: &mockedDescribeNetworkInterfacesClient{}, - DescribeTasksClient: &mockedDescribeTasksClient{}, - ListTasksClient: &mockedListTasksClient{}, - ListClustersClient: &mockedListclustersClient{}, - DescribeTaskDefinitionClient: &mockedDescribeTaskDefinitionInterface{}, - //IAMSimulatePrincipalPolicyClient: &mockedDescribeTaskDefinitionsClient{}, + AWSProfile: "default", + AWSRegions: []string{"us-east-1", "us-west-1"}, + Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")}, + SkipAdminCheck: true, + Goroutines: 30, + EC2Client: &sdk.MockedEC2Client2{}, + ECSClient: &sdk.MockedECSClient{}, }, expectedResult: []MappedECSTask{{ Cluster: "MyCluster", @@ -281,7 +43,7 @@ func TestECSTasks(t *testing.T) { internal.MockFileSystem(true) for _, subtest := range subtests { t.Run(subtest.name, func(t *testing.T) { - subtest.testModule.ECSTasks(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity) + subtest.testModule.ECSTasks(subtest.outputDirectory, subtest.verbosity) for index, expectedTask := range subtest.expectedResult { if expectedTask.Cluster != subtest.testModule.MappedECSTasks[index].Cluster { log.Fatal("Cluster name does not match expected value") diff --git a/aws/eks.go b/aws/eks.go index d9edc5e..61563a7 100644 --- a/aws/eks.go +++ b/aws/eks.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" @@ -21,9 +22,11 @@ type EKSModule struct { EKSClient sdk.EKSClientInterface IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + Goroutines int AWSProfile string SkipAdminCheck bool @@ -52,8 +55,8 @@ type Cluster struct { CanPrivEsc string } -func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *EKSModule) EKS(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "eks" @@ -122,23 +125,48 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i } } - // add - if struct is not empty do this. otherwise, dont write anything. - if m.pmapperError == nil { - m.output.Headers = []string{ - "Service", + // This is the complete list of potential table columns + m.output.Headers = []string{ + "Account", + "Region", + "Name", + //"Endpoint", + "Public", + //"OIDC", + "NodeGroup", + "Role", + "IsAdminRole?", + "CanPrivEscToAdmin?", + } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", "Region", "Name", - //"Endpoint", + "Endpoint", "Public", - //"OIDC", + "OIDC", "NodeGroup", "Role", "IsAdminRole?", "CanPrivEscToAdmin?", } + // Otherwise, use the default columns. } else { - m.output.Headers = []string{ - "Service", + tableCols = []string{ "Region", "Name", //"Endpoint", @@ -147,47 +175,34 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i "NodeGroup", "Role", "IsAdminRole?", - //"CanPrivEscToAdmin?", + "CanPrivEscToAdmin?", } } + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") + } + // Table rows for i := range m.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, - }, - ) - - } + m.output.Body = append( + m.output.Body, + []string{ + aws.ToString(m.Caller.Account), + m.Clusters[i].Region, + m.Clusters[i].Name, + //m.Clusters[i].Endpoint, + m.Clusters[i].Public, + //m.Clusters[i].OIDC, + m.Clusters[i].NodeGroup, + m.Clusters[i].Role, + m.Clusters[i].Admin, + m.Clusters[i].CanPrivEsc, + }, + ) } var seen []string @@ -199,10 +214,6 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -211,9 +222,10 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/eks_test.go b/aws/eks_test.go index 8e1311a..04ae406 100644 --- a/aws/eks_test.go +++ b/aws/eks_test.go @@ -86,7 +86,6 @@ func TestEks(t *testing.T) { EKSClient: &MockedEKSClientInterface{}, //IAMSimulatePrincipalPolicyClient: iam.SimulatePrincipalPolicyAPIClient, Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")}, - OutputFormat: "table", AWSProfile: "test", Goroutines: 30, AWSRegions: AWSRegions, @@ -104,7 +103,7 @@ func TestEks(t *testing.T) { internal.MockFileSystem(true) for _, subtest := range subtests { t.Run(subtest.name, func(t *testing.T) { - subtest.testModule.EKS(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity) + subtest.testModule.EKS(subtest.outputDirectory, subtest.verbosity) for index, expectedCluster := range subtest.expectedResult { if expectedCluster.Name != subtest.testModule.Clusters[index].Name { log.Fatal("Cluster name does not match expected value") diff --git a/aws/elastic-network-interfaces.go b/aws/elastic-network-interfaces.go index f53e861..8f9acda 100644 --- a/aws/elastic-network-interfaces.go +++ b/aws/elastic-network-interfaces.go @@ -1,16 +1,16 @@ package aws import ( - "context" "fmt" "os" "path/filepath" "strconv" + "strings" "sync" + "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/bishopfox/awsservicemap" @@ -18,14 +18,15 @@ import ( ) type ElasticNetworkInterfacesModule struct { - //EC2Client *ec2.Client - DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient + EC2Client sdk.AWSEC2ClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + AWSProfile string + WrapTable bool MappedENIs []MappedENI CommandCounter internal.CommandCounter @@ -44,7 +45,7 @@ type MappedENI struct { Description string } -func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputFormat string, outputDirectory string, verbosity int) { +func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputDirectory string, verbosity int) { m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "elastic-network-interfaces" @@ -84,7 +85,7 @@ func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputFormat s receiverDone <- true <-receiverDone - m.printENIsData(outputFormat, outputDirectory, dataReceiver, verbosity) + m.printENIsData(outputDirectory, dataReceiver, verbosity) } @@ -101,8 +102,9 @@ func (m *ElasticNetworkInterfacesModule) Receiver(receiver chan MappedENI, recei } } -func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outputDirectory string, dataReceiver chan MappedENI, verbosity int) { +func (m *ElasticNetworkInterfacesModule) printENIsData(outputDirectory string, dataReceiver chan MappedENI, verbosity int) { m.output.Headers = []string{ + "Account", "ID", "Type", "External IP", @@ -111,10 +113,48 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outp "Attached Instance", "Description", } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "ID", + "Type", + "External IP", + "Internal IP", + "VPC ID", + "Attached Instance", + "Description", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "ID", + "Type", + "External IP", + "Internal IP", + "VPC ID", + "Attached Instance", + "Description", + } + } + for _, eni := range m.MappedENIs { m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), eni.ID, eni.Type, eni.ExternalIP, @@ -127,10 +167,7 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outp } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -139,9 +176,10 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outp }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -213,50 +251,45 @@ func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGr } func (m *ElasticNetworkInterfacesModule) getDescribeNetworkInterfaces(region string, dataReceiver chan MappedENI) { - var PaginationControl *string - for { - DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces( - context.TODO(), - &(ec2.DescribeNetworkInterfacesInput{ - NextToken: PaginationControl, - }), - func(o *ec2.Options) { - o.Region = region - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - for _, eni := range DescribeNetworkInterfaces.NetworkInterfaces { - status := string(eni.Status) - if status == "available" { - continue // unused ENI - } - - mappedENI := MappedENI{ - ID: aws.ToString(eni.NetworkInterfaceId), - Type: string(eni.InterfaceType), - ExternalIP: getPublicIPOfElasticNetworkInterface(eni), - PrivateIP: aws.ToString(eni.PrivateIpAddress), - VPCID: aws.ToString(eni.VpcId), - AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni), - Description: aws.ToString(eni.Description), - } - - dataReceiver <- mappedENI + NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region) + + // var PaginationControl *string + // for { + // DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces( + // context.TODO(), + // &(ec2.DescribeNetworkInterfacesInput{ + // NextToken: PaginationControl, + // }), + // func(o *ec2.Options) { + // o.Region = region + // }, + // ) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + for _, eni := range NetworkInterfaces { + status := string(eni.Status) + if status == "available" { + continue // unused ENI } - if DescribeNetworkInterfaces.NextToken != nil { - PaginationControl = DescribeNetworkInterfaces.NextToken - } else { - PaginationControl = nil - break + mappedENI := MappedENI{ + ID: aws.ToString(eni.NetworkInterfaceId), + Type: string(eni.InterfaceType), + ExternalIP: getPublicIPOfElasticNetworkInterface(eni), + PrivateIP: aws.ToString(eni.PrivateIpAddress), + VPCID: aws.ToString(eni.VpcId), + AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni), + Description: aws.ToString(eni.Description), } + dataReceiver <- mappedENI } + } func getPublicIPOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string { diff --git a/aws/elastic-network-interfaces_test.go b/aws/elastic-network-interfaces_test.go index c9c0f22..1ec02f1 100644 --- a/aws/elastic-network-interfaces_test.go +++ b/aws/elastic-network-interfaces_test.go @@ -1,48 +1,59 @@ package aws import ( - "context" - "encoding/json" - "log" "testing" + "github.com/BishopFox/cloudfox/aws/sdk" + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ec2" - ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go-v2/service/sts" ) -type mockedDescribeNetworkInterfacesClient2 struct { - describeNetworkInterfaces DescribeNetworkInterfaces -} +func TestElasticNetworkInterfaces(t *testing.T) { -func (c *mockedDescribeNetworkInterfacesClient2) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) { - var nics []ec2types.NetworkInterface - err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces) - if err != nil { - log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE) + m := ElasticNetworkInterfacesModule{ + AWSProfile: "default", + AWSRegions: []string{"us-east-1", "us-west-1"}, + Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")}, + EC2Client: &sdk.MockedEC2Client2{}, } - for _, mockednic := range c.describeNetworkInterfaces.NetworkInterfaces { - nics = append(nics, ec2types.NetworkInterface{ - Association: &ec2types.NetworkInterfaceAssociation{ - PublicIp: aws.String(mockednic.Association.PublicIP), + + //m.ElasticNetworkInterfaces("table", ".", 3) + subtests := []struct { + name string + testModule ElasticNetworkInterfacesModule + expectedResult []MappedENI + }{ + { + name: "Test ElasticNetworkInterfaces", + testModule: m, + expectedResult: []MappedENI{ + { + PrivateIP: "10.0.1.17", + ExternalIP: "203.0.113.12", + }, + { + PrivateIP: "10.0.1.149", + ExternalIP: "198.51.100.0", + }, }, - NetworkInterfaceId: aws.String(mockednic.NetworkInterfaceID), - PrivateIpAddress: aws.String(mockednic.PrivateIPAddress), - VpcId: aws.String(mockednic.VpcID), - Attachment: &ec2types.NetworkInterfaceAttachment{InstanceId: aws.String(mockednic.Attachment.InstanceID)}, - Description: aws.String(mockednic.Description), - }) + }, } - return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil -} + internal.MockFileSystem(true) + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + subtest.testModule.ElasticNetworkInterfaces(".", 3) + for index, expectedTask := range subtest.expectedResult { + if expectedTask.ExternalIP != subtest.testModule.MappedENIs[index].ExternalIP { + t.Errorf("expected %s, got %s", expectedTask.ExternalIP, subtest.testModule.MappedENIs[index].ExternalIP) + } + if expectedTask.PrivateIP != subtest.testModule.MappedENIs[index].PrivateIP { + t.Errorf("expected %s, got %s", expectedTask.PrivateIP, subtest.testModule.MappedENIs[index].PrivateIP) + } -func TestElasticNetworkInterfaces(t *testing.T) { - m := ElasticNetworkInterfacesModule{ - AWSProfile: "default", - AWSRegions: []string{"us-east-1", "us-west-1"}, - Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")}, - DescribeNetworkInterfacesClient: &mockedDescribeNetworkInterfacesClient2{}, + } + }) } - m.ElasticNetworkInterfaces("table", ".", 3) } diff --git a/aws/endpoints.go b/aws/endpoints.go index 9a3d0f8..37b8f1c 100644 --- a/aws/endpoints.go +++ b/aws/endpoints.go @@ -55,12 +55,14 @@ type EndpointsModule struct { AppRunnerClient *apprunner.Client LightsailClient *lightsail.Client - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data Endpoints []Endpoint @@ -83,8 +85,8 @@ type Endpoint struct { var oe *smithy.OperationError -func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *EndpointsModule) PrintEndpoints(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "endpoints" @@ -147,6 +149,7 @@ func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory st }) m.output.Headers = []string{ + "Account", "Service", "Region", "Name", @@ -156,11 +159,48 @@ func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory st "Public", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Service", + "Region", + "Name", + "Endpoint", + "Port", + "Protocol", + "Public", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Service", + "Region", + "Name", + "Endpoint", + "Port", + "Protocol", + "Public", + } + } + // Table rows for i := range m.Endpoints { m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Endpoints[i].AWSService, m.Endpoints[i].Region, m.Endpoints[i].Name, @@ -174,10 +214,6 @@ func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory st } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -186,9 +222,10 @@ func (m *EndpointsModule) PrintEndpoints(outputFormat string, outputDirectory st }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -545,7 +582,7 @@ func (m *EndpointsModule) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, se m.CommandCounter.Error++ continue } - if BrokerDetails.PubliclyAccessible { + if aws.ToBool(BrokerDetails.PubliclyAccessible) { public = "True" } else { public = "False" @@ -615,7 +652,7 @@ func (m *EndpointsModule) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, s var endpoint string var kibana_endpoint string - // This exits thie function if an opensearch domain exists but there is no endpoint + // This exits the function if an opensearch domain exists but there is no endpoint if raw_endpoint == nil { return } else { @@ -1386,7 +1423,7 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, endpoint := aws.ToString(instance.Endpoint.Address) awsService := "RDS" - if instance.PubliclyAccessible { + if aws.ToBool(instance.PubliclyAccessible) { public = "True" } else { public = "False" @@ -1397,7 +1434,7 @@ func (m *EndpointsModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, Region: r, Name: name, Endpoint: endpoint, - Port: port, + Port: aws.ToInt32(port), Protocol: aws.ToString(instance.Engine), Public: public, } @@ -1442,7 +1479,7 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG //id := workspace.Id endpoint := aws.ToString(cluster.Endpoint.Address) - if cluster.PubliclyAccessible { + if aws.ToBool(cluster.PubliclyAccessible) { public = "True" } else { public = "False" @@ -1455,7 +1492,7 @@ func (m *EndpointsModule) getRedshiftEndPointsPerRegion(r string, wg *sync.WaitG Region: r, Name: name, Endpoint: endpoint, - Port: port, + Port: aws.ToInt32(port), Protocol: protocol, Public: public, } diff --git a/aws/env-vars.go b/aws/env-vars.go index 6594b06..dde3ed8 100644 --- a/aws/env-vars.go +++ b/aws/env-vars.go @@ -6,6 +6,7 @@ import ( "path/filepath" "sort" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" @@ -34,12 +35,14 @@ import ( type EnvsModule struct { // General configuration data - Caller sts.GetCallerIdentityOutput - AWSRegions []string - AWSProfile string - OutputFormat string - Goroutines int - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSProfile string + AWSOutputType string + AWSTableCols string + + Goroutines int + WrapTable bool // Service Clients ECSClient *ecs.Client @@ -65,8 +68,8 @@ type EnvironmentVariable struct { environmentVarValue string } -func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "env-vars" @@ -119,6 +122,7 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb // Table headers m.output.Headers = []string{ + "Account", "Service", "Region", "Name", @@ -126,10 +130,42 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb "Value", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Service", + "Region", + "Name", + "Key", + "Value", + } + } else { + tableCols = []string{ + "Service", + "Region", + "Name", + "Key", + "Value", + } + } + //Table rows for _, envVar := range m.EnvironmentVariables { m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), envVar.service, envVar.region, envVar.name, @@ -141,9 +177,7 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -152,9 +186,10 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/filesystems.go b/aws/filesystems.go index 61c7145..206f65b 100644 --- a/aws/filesystems.go +++ b/aws/filesystems.go @@ -7,6 +7,7 @@ import ( "path/filepath" "sort" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" @@ -27,12 +28,14 @@ type FilesystemsModule struct { EFSClient *efs.Client FSxClient *fsx.Client - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data Filesystems []FilesystemObject @@ -55,8 +58,8 @@ type FilesystemObject struct { Permissions string } -func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "filesystems" @@ -107,7 +110,7 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector }) m.output.Headers = []string{ - "Service", + "Account", "Region", "Name", "DNS Name", @@ -117,12 +120,48 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector "Permissions", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Region", + "Name", + "DNS Name", + //"IP", + "Mount Target", + "Policy", + "Permissions", + } + } else { + tableCols = []string{ + "Region", + "Name", + "DNS Name", + //"IP", + "Mount Target", + "Policy", + "Permissions", + } + + } + // Table rows for i := range m.Filesystems { m.output.Body = append( m.output.Body, []string{ - m.Filesystems[i].AWSService, + aws.ToString(m.Caller.Account), m.Filesystems[i].Region, m.Filesystems[i].Name, m.Filesystems[i].DnsName, @@ -137,10 +176,7 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -149,9 +185,10 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/iam-simulator.go b/aws/iam-simulator.go index c96d0f8..0b7e258 100644 --- a/aws/iam-simulator.go +++ b/aws/iam-simulator.go @@ -7,6 +7,7 @@ import ( "path/filepath" "sort" "strconv" + "strings" "sync" "time" @@ -20,13 +21,15 @@ import ( type IamSimulatorModule struct { // General configuration data - IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + IAMClient sdk.AWSIAMClientInterface + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data SimulatorResults []SimulatorResult @@ -64,9 +67,9 @@ var ( TxtLogger = internal.TxtLogger() ) -func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputFormat string, outputDirectory string, verbosity int) { +func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "iam-simulator" @@ -149,11 +152,36 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, // Regardless of what options were selected, for now at least, we will always print the data using the output module (table/csv mode) m.output.Headers = []string{ - "Service", + "Account", "Principal", "Query", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Principal", + "Query", + } + } else { + tableCols = []string{ + "Principal", + "Query", + } + } + sort.Slice(m.SimulatorResults, func(i, j int) bool { return m.SimulatorResults[i].Query < m.SimulatorResults[j].Query }) @@ -163,7 +191,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, m.output.Body = append( m.output.Body, []string{ - m.SimulatorResults[i].AWSService, + aws.ToString(m.Caller.Account), m.SimulatorResults[i].Principal, m.SimulatorResults[i].Query, }, @@ -172,8 +200,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -182,9 +209,10 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/instances.go b/aws/instances.go index 92f5f9a..8ce1e89 100644 --- a/aws/instances.go +++ b/aws/instances.go @@ -13,7 +13,6 @@ import ( "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/aws/aws-sdk-go-v2/service/iam" iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" @@ -24,11 +23,13 @@ import ( type InstancesModule struct { // General configuration data - EC2Client *ec2.Client - IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string + EC2Client sdk.AWSEC2ClientInterface + IAMClient sdk.AWSIAMClientInterface + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + Goroutines int UserDataAttributesOnly bool AWSProfile string @@ -63,8 +64,8 @@ type MappedInstance struct { CanPrivEsc string } -func (m *InstancesModule) Instances(filter string, outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *InstancesModule) Instances(filter string, outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "instances" @@ -147,9 +148,9 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi // This conditional block will either dump the userData attribute content or the general instances data, depending on what you select via command line. //fmt.Printf("\n[*] Preparing output...\n\n") if m.UserDataAttributesOnly { - m.printInstancesUserDataAttributesOnly(outputFormat, outputDirectory, dataReceiver) + m.printInstancesUserDataAttributesOnly(outputDirectory, dataReceiver) } else { - m.printGeneralInstanceData(outputFormat, outputDirectory, dataReceiver, verbosity) + m.printGeneralInstanceData(outputDirectory, dataReceiver, verbosity) } } @@ -167,7 +168,7 @@ func (m *InstancesModule) Receiver(receiver chan MappedInstance, receiverDone ch } } -func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) { +func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputDirectory string, dataReceiver chan MappedInstance) { defer func() { m.output.CallingModule = "instances" }() @@ -217,12 +218,39 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri } } -func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance, verbosity int) { +func (m *InstancesModule) printGeneralInstanceData(outputDirectory string, dataReceiver chan MappedInstance, verbosity int) { // Prepare Table headers //m.output.Headers = table.Row{ - if m.pmapperError == nil { + m.output.Headers = []string{ + "Account", + //"ID", + "Name", + //"Arn", + "ID", + "Zone", + "State", + "External IP", + "Internal IP", + "Role", + "IsAdminRole?", + "CanPrivEscToAdmin?", + } - m.output.Headers = []string{ + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", //"ID", "Name", //"Arn", @@ -235,8 +263,9 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi "IsAdminRole?", "CanPrivEscToAdmin?", } + // Otherwise, use the default columns. } else { - m.output.Headers = []string{ + tableCols = []string{ //"ID", "Name", //"Arn", @@ -247,59 +276,40 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi "Internal IP", "Role", "IsAdminRole?", - //"CanPrivEscToAdmin?", + "CanPrivEscToAdmin?", } } - //Table rows - if m.pmapperError == nil { + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") + } - for _, 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, - }, - ) - } + //Table rows + for _, instance := range m.MappedInstances { + m.output.Body = append( + m.output.Body, + //table.Row{ + []string{ + aws.ToString(m.Caller.Account), + //instance.ID, + instance.Name, + //instance.Arn, + instance.ID, + instance.AvailabilityZone, + instance.State, + instance.ExternalIP, + instance.PrivateIP, + instance.Role, + instance.Admin, + instance.CanPrivEsc, + }, + ) } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - ////m.output.OutputSelector(outputFormat) - //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) //m.writeLoot(m.output.FilePath) o := internal.OutputClient{ @@ -310,9 +320,10 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -440,27 +451,17 @@ func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg } func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, region string) (userData *string, err error) { - - Attributes, err := m.EC2Client.DescribeInstanceAttribute( - context.TODO(), - &ec2.DescribeInstanceAttributeInput{ - InstanceId: instanceID, - Attribute: types.InstanceAttributeName("userData"), - }, - func(o *ec2.Options) { - o.Region = region - }, - ) + UserData, err := sdk.CachedEC2DescribeInstanceAttributeUserData(m.EC2Client, aws.ToString(m.Caller.Account), region, aws.ToString(instanceID)) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ return nil, err } else { - if Attributes.UserData.Value == nil { + if UserData == "" { return aws.String("NoUserData"), nil } else { - data, _ := base64.StdEncoding.DecodeString(*Attributes.UserData.Value) + data, _ := base64.StdEncoding.DecodeString(UserData) return aws.String(string(data)), nil } } @@ -470,47 +471,22 @@ func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, regio func (m *InstancesModule) getDescribeInstances(instancesToSearch []string, region string, dataReceiver chan MappedInstance) { // The "PaginationControl" value is nil when there's no more data to return. - var PaginationControl *string - for { - DescribeInstances, err := m.EC2Client.DescribeInstances( - context.TODO(), - &(ec2.DescribeInstancesInput{ - NextToken: PaginationControl, - }), - func(o *ec2.Options) { - o.Region = region - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - - for _, reservation := range DescribeInstances.Reservations { - accountId := reservation.OwnerId - - for _, instance := range reservation.Instances { - - if instancesToSearch[0] == "all" || internal.Contains(aws.ToString(instance.InstanceId), instancesToSearch) { - m.loadInstanceData(instance, region, accountId, dataReceiver) - } - } - } + Instances, err := sdk.CachedEC2DescribeInstances(m.EC2Client, aws.ToString(m.Caller.Account), region) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - // The "NextToken" value is nil when there's no more data to return. - if DescribeInstances.NextToken != nil { - PaginationControl = DescribeInstances.NextToken - } else { - PaginationControl = nil - break + for _, instance := range Instances { + if instancesToSearch[0] == "all" || internal.Contains(aws.ToString(instance.InstanceId), instancesToSearch) { + m.loadInstanceData(instance, region, dataReceiver) } - } } -func (m *InstancesModule) loadInstanceData(instance types.Instance, region string, accountId *string, dataReceiver chan MappedInstance) { +func (m *InstancesModule) loadInstanceData(instance types.Instance, region string, dataReceiver chan MappedInstance) { var profile string var externalIP string @@ -552,7 +528,7 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin dataReceiver <- MappedInstance{ ID: aws.ToString(instance.InstanceId), Name: aws.ToString(&name), - Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(accountId), aws.ToString(instance.InstanceId)), + Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)), AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone), State: string(instance.State.Name), ExternalIP: externalIP, diff --git a/aws/inventory.go b/aws/inventory.go index 5a89a45..13d430e 100644 --- a/aws/inventory.go +++ b/aws/inventory.go @@ -1,7 +1,6 @@ package aws import ( - "context" "fmt" "os" "path/filepath" @@ -15,15 +14,15 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigatewayv2" "github.com/aws/aws-sdk-go-v2/service/apprunner" + "github.com/aws/aws-sdk-go-v2/service/athena" + "github.com/aws/aws-sdk-go-v2/service/cloud9" "github.com/aws/aws-sdk-go-v2/service/cloudformation" "github.com/aws/aws-sdk-go-v2/service/cloudfront" - "github.com/aws/aws-sdk-go-v2/service/codebuild" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/aws/aws-sdk-go-v2/service/glue" "github.com/aws/aws-sdk-go-v2/service/grafana" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/lambda" @@ -31,7 +30,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/mq" "github.com/aws/aws-sdk-go-v2/service/opensearch" "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/aws/aws-sdk-go-v2/service/redshift" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/aws/aws-sdk-go-v2/service/sns" @@ -44,40 +42,54 @@ import ( type Inventory2Module struct { // General configuration data - LambdaClient *lambda.Client - EC2Client *ec2.Client - ECSClient *ecs.Client - EKSClient sdk.EKSClientInterface - S3Client *s3.Client - CloudFormationClient *cloudformation.Client - SecretsManagerClient *secretsmanager.Client - SSMClient *ssm.Client - RDSClient *rds.Client - APIGatewayv2Client *apigatewayv2.Client - ELBv2Client *elasticloadbalancingv2.Client - ELBClient *elasticloadbalancing.Client - IAMClient *iam.Client - MQClient *mq.Client - OpenSearchClient *opensearch.Client - GrafanaClient *grafana.Client - APIGatewayClient *apigateway.Client - RedshiftClient *redshift.Client - CloudfrontClient *cloudfront.Client - AppRunnerClient *apprunner.Client - LightsailClient *lightsail.Client - GlueClient *glue.Client - SNSClient *sns.Client - SQSClient *sqs.Client - DynamoDBClient *dynamodb.Client - CodeBuildClient sdk.CodeBuildClientInterface - StepFunctionClient sdk.StepFunctionsClientInterface - - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + APIGatewayClient *apigateway.Client + APIGatewayv2Client *apigatewayv2.Client + AppRunnerClient *apprunner.Client + AthenaClient *athena.Client + Cloud9Client *cloud9.Client + CloudFormationClient *cloudformation.Client + CloudfrontClient *cloudfront.Client + CodeArtifactClient sdk.AWSCodeArtifactClientInterface + CodeBuildClient sdk.CodeBuildClientInterface + CodeCommitClient sdk.AWSCodeCommitClientInterface + CodeDeployClient sdk.AWSCodeDeployClientInterface + DataPipelineClient sdk.AWSDataPipelineClientInterface + DynamoDBClient *dynamodb.Client + EC2Client *ec2.Client + ECRClient sdk.AWSECRClientInterface + ECSClient *ecs.Client + EKSClient sdk.EKSClientInterface + ELBClient *elasticloadbalancing.Client + ELBv2Client *elasticloadbalancingv2.Client + ElasticacheClient sdk.AWSElastiCacheClientInterface + ElasticBeanstalkClient sdk.AWSElasticBeanstalkClientInterface + EMRClient sdk.AWSEMRClientInterface + GrafanaClient *grafana.Client + GlueClient sdk.AWSGlueClientInterface + KinesisClient sdk.AWSKinesisClientInterface + IAMClient *iam.Client + LambdaClient *lambda.Client + LightsailClient *lightsail.Client + MQClient *mq.Client + OpenSearchClient *opensearch.Client + RDSClient *rds.Client + RedshiftClient sdk.AWSRedShiftClientInterface + Route53Client sdk.AWSRoute53ClientInterface + S3Client *s3.Client + SQSClient *sqs.Client + SSMClient *ssm.Client + SNSClient *sns.Client + SecretsManagerClient *secretsmanager.Client + StepFunctionClient sdk.StepFunctionsClientInterface + + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data RegionResourceCount int @@ -101,9 +113,9 @@ type GlobalResourceCount2 struct { count int } -func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDirectory string, verbosity int) { +func (m *Inventory2Module) PrintInventoryPerRegion(outputDirectory string, verbosity int) { - // These stuct values are used by the output module + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "inventory" @@ -111,8 +123,67 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi "module": "inventory", }, ) - // def change this to build dynamically in the future. - m.services = []string{"total", "APIGateway RestAPIs", "APIGatewayv2 APIs", "AppRunner Services", "CloudFormation Stacks", "Cloudfront Distributions", "CodeBuild Projects", "DynamoDB Tables", "EC2 Instances", "ECS Tasks", "EKS Clusters", "ELB Load Balancers", "ELBv2 Load Balancers", "Glue Dev Endpoints", "Glue Jobs", "Grafana Workspaces", "Lambda Functions", "Lightsail Instances/Containers", "MQ Brokers", "OpenSearch DomainNames", "RDS DB Instances", "SecretsManager Secrets", "SNS Topics", "SQS Queues", "SSM Parameters", "StepFunctions State Machines"} + + m.services = []string{ + "total", + "APIGateway RestAPIs", + "APIGatewayv2 APIs", + "Athena Databases", + //"Athena Data Catalogs", + "AppRunner Services", + "Cloud9 Environments", + "CloudFormation Stacks", + "Cloudfront Distributions", + "CodeArtifact Repositories", + "CodeArtifact Domains", + "CodeBuild Projects", + "CodeCommit Repositories", + "CodeDeploy Applications", + "CodeDeploy Deployments", + "DataPipeline Pipelines", + "DynamoDB Tables", + "EC2 Instances", + "EC2 AMIs", + "EC2 Volumes", + "EC2 Snapshots", + "ECS Clusters", + "ECS Tasks", + "ECS Services", + "ECR Repositories", + "EKS Clusters", + "EKS Cluster NodeGroups", + "Elasticache Clusters", + "ElasticBeanstalk Applications", + "ELB Load Balancers", + "ELBv2 Load Balancers", + "EMR Clusters", + "EMR Instances", + "Glue Databases", + "Glue Dev Endpoints", + "Glue Jobs", + "Glue Tables", + "Grafana Workspaces", + "IAM Access Keys", + "IAM Roles", + "IAM Users", + "IAM Groups", + "Kinesis Data Streams", + "Lambda Functions", + "Lightsail Instances/Containers", + "MQ Brokers", + "OpenSearch DomainNames", + "Redshift Clusters", + "RDS DB Instances", + "Route53 Zones", + "Route53 Records", + "S3 Buckets", + "SecretsManager Secrets", + "SNS Topics", + "SQS Queues", + "SSM Parameters", + "StepFunctions State Machines", + } + m.serviceMap = map[string]map[string]int{} m.totalRegionCounts = map[string]int{} @@ -128,12 +199,13 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi m.serviceMap[service][region] = 0 m.totalRegionCounts[region] = 0 } + m.serviceMap[service]["Global"] = 0 } fmt.Printf("[%s][%s] Enumerating selected services in all regions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) fmt.Printf("[%s][%s] Supported Services: ApiGateway, ApiGatewayv2, AppRunner, CloudFormation, Cloudfront, CodeBuild, DynamoDB, \n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - fmt.Printf("[%s][%s] \t\t\tEC2, ECS, EKS, ELB, ELBv2, Glue, Grafana, IAM, Lambda, Lightsail, MQ, \n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - fmt.Printf("[%s][%s] \t\t\tOpenSearch, RDS, S3, SecretsManager, SNS, SQS, SSM, Step Functions\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) + fmt.Printf("[%s][%s] \t\t\tEC2, ECS, ECR, EKS, ELB, ELBv2, Glue, Grafana, IAM, Lambda, Lightsail, MQ, \n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) + fmt.Printf("[%s][%s] \t\t\tOpenSearch, RedShift, RDS, Route53, S3, SecretsManager, SNS, SQS, SSM, Step Functions\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) wg := new(sync.WaitGroup) semaphore := make(chan struct{}, m.Goroutines) @@ -160,23 +232,23 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi } - // for _, r := range []string{"us-east-1", "us-east-2", "ap-northeast-1", "eu-west-1", "us-west-2"} { - // m.CommandCounter.Total++ - // wg.Add(1) - // go m.getAppRunnerServicesPerRegion(r, wg, semaphore) - // } + // Time for the non-concurrent global checks + m.getBuckets(verbosity, dataReceiver) + m.getIAMUsers(verbosity, dataReceiver) + m.getIAMRoles(verbosity, dataReceiver) + m.getIAMGroups(verbosity, dataReceiver) + m.getIAMAccessKeys(verbosity, dataReceiver) + m.getCloudfrontDistros(verbosity, dataReceiver) + m.getRoute53Zones(verbosity, dataReceiver) + m.getRoute53Records(verbosity, dataReceiver) wg.Wait() - //time.Sleep(time.Second * 2) // Send a message to the spinner goroutine to close the channel and stop spinnerDone <- true <-spinnerDone - //duration := time.Since(start) - //fmt.Printf("\n\n[*] Total execution time %s\n", duration) - - // This creates the header row (columns) dynamically - a region oly gets printed if it has at least one resource. + // This creates the header row (columns) dynamically - a region only gets printed if it has at least one resource. m.output.Headers = append(m.output.Headers, "Resource Type") type kv struct { @@ -193,6 +265,14 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi return ss[i].Value > ss[j].Value }) + // move the Global column to the front + for i, v := range ss { + if v.Key == "Global" { + ss[0], ss[i] = ss[i], ss[0] + } + } + + //add the regions to the header row for _, region := range ss { if region.Value != 0 { @@ -218,15 +298,6 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi } m.output.Body = append(m.output.Body, totalRow) - // var sortedBody []kv - // for k, v := range m.serviceMap { - // sortedBody = append(sortedBody, kv{k, v}) - // } - - // sort.Slice(sortedBody, func(i, j int) bool { - // return sortedBody[i].Key > ss[j].Key - // }) - // This is where we create the per service row with variable number of columns as well, using the same logic we used for the header for _, service := range m.services { if service != "total" { @@ -244,14 +315,26 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi } } - // Convert the slice of strings to a slice of interfaces??? not sure, but this was needed. I couldnt just pass temp row to the output.Body - for _, val := range temprow { - outputRow = append(outputRow, val) + // check to see if all regions have no resources for the service. Skip the first column, which is the resource type. + // If any value is other than "-" set rowEmpty to false. + var rowEmtpy bool = true + for _, val := range temprow[1:] { + + if val != "-" { + rowEmtpy = false + } } + // If rowEmpty is still true at the end of the row, we dont add the row to the output, otherwise we do. + if !rowEmtpy { + // Convert the slice of strings to a slice of interfaces??? not sure, but this was needed. I couldn't just pass temp row to the output.Body + for _, val := range temprow { + outputRow = append(outputRow, val) - // Finally write the row to the table - m.output.Body = append(m.output.Body, outputRow) + } + // Finally write the row to the table if the service has at least one resource + m.output.Body = append(m.output.Body, outputRow) + } } } @@ -263,9 +346,6 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi // } if len(m.output.Body) > 0 { - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -281,18 +361,11 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - var header []string - var body [][]string - header, body = m.PrintGlobalResources(outputFormat, outputDirectory, verbosity, dataReceiver) - o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: header, - Body: body, - Name: "inventory-global", - }) + o.WriteFullOutput(o.Table.TableFiles, nil) m.writeLoot(o.Table.DirectoryName, verbosity) - m.PrintTotalResources(outputFormat) + m.PrintTotalResources(m.AWSOutputType) //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)) @@ -302,42 +375,6 @@ func (m *Inventory2Module) PrintInventoryPerRegion(outputFormat string, outputDi <-receiverDone } -func (m *Inventory2Module) PrintGlobalResources(outputFormat string, outputDirectory string, verbosity int, dataReceiver chan GlobalResourceCount2) ([]string, [][]string) { - m.globalOutput.Verbosity = verbosity - m.globalOutput.CallingModule = "inventory" - m.globalOutput.FullFilename = "inventory-global" - - m.getBuckets(verbosity, dataReceiver) - m.getIAMUsers(verbosity, dataReceiver) - m.getIAMRoles(verbosity, dataReceiver) - m.getCloudfrontDistros(verbosity, dataReceiver) - - //m.globalOutput.CallingModule = fmt.Sprintf("%s-global", m.globalOutput.CallingModule) - m.globalOutput.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile) - - m.globalOutput.Headers = []string{ - "Resource Type", - "Total", - } - - for i, GlobalResourceCount := range m.GlobalResourceCounts { - if m.GlobalResourceCounts[i].count != 0 { - m.globalOutput.Body = append( - m.globalOutput.Body, - []string{ - GlobalResourceCount.resourceType, - strconv.Itoa(GlobalResourceCount.count), - }, - ) - } - } - //m.globalOutput.FilePath = filepath.Join(path, m.globalOutput.CallingModule) - //m.globalOutput.OutputSelector(outputFormat) - //internal.OutputSelector(verbosity, outputFormat, m.globalOutput.Headers, m.globalOutput.Body, m.globalOutput.FilePath, m.globalOutput.FullFilename, m.globalOutput.CallingModule, false, m.AWSProfile) - return m.globalOutput.Headers, m.globalOutput.Body - -} - func (m *Inventory2Module) writeLoot(outputDirectory string, verbosity int) { path := filepath.Join(outputDirectory, "loot") err := os.MkdirAll(path, os.ModePerm) @@ -390,24 +427,45 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore JsonFileSource: "DOWNLOAD_FROM_AWS", } - res, err := servicemap.IsServiceInRegion("lambda", r) + // AppRunner is not supported in the aws service region catalog so we have to run it in all regions + m.CommandCounter.Total++ + wg.Add(1) + go m.getAppRunnerServicesPerRegion(r, wg, semaphore) + + res, err := servicemap.IsServiceInRegion("apigateway", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getLambdaFunctionsPerRegion(r, wg, semaphore) + go m.getAPIGatewayvAPIsPerRegion(r, wg, semaphore) + + m.CommandCounter.Total++ + wg.Add(1) + go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("ec2", r) + res, err = servicemap.IsServiceInRegion("athena", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getEc2InstancesPerRegion(r, wg, semaphore) + go m.getAthenaDatabasesPerRegion(r, wg, semaphore) + // wg.Add(1) + // go m.getAthenaDataCatalogsPerRegion(r, wg, semaphore) + } + + res, err = servicemap.IsServiceInRegion("cloud9", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getCloud9EnvironmentsPerRegion(r, wg, semaphore) } res, err = servicemap.IsServiceInRegion("cloudformation", r) @@ -420,58 +478,120 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore go m.getCloudFormationStacksPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("secretsmanager", r) + res, err = servicemap.IsServiceInRegion("codeartifact", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getSecretsManagerSecretsPerRegion(r, wg, semaphore) + go m.getCodeArtifactDomainsPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("eks", r) + res, err = servicemap.IsServiceInRegion("codebuild", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getEksClustersPerRegion(r, wg, semaphore) + go m.getCodeBuildProjectsPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("ecs", r) + res, err = servicemap.IsServiceInRegion("codecommit", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getEcsTasksPerRegion(r, wg, semaphore) + go m.getCodeCommitRepositoriesPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("rds", r) + res, err = servicemap.IsServiceInRegion("codedeploy", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getRdsClustersPerRegion(r, wg, semaphore) + go m.getCodeDeployApplicationsPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getCodeDeployDeploymentsPerRegion(r, wg, semaphore) + } - res, err = servicemap.IsServiceInRegion("apigateway", r) + res, err = servicemap.IsServiceInRegion("datapipeline", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getAPIGatewayvAPIsPerRegion(r, wg, semaphore) + go m.getDataPipelinePipelinesPerRegion(r, wg, semaphore) + } + res, err = servicemap.IsServiceInRegion("dynamodb", r) + if err != nil { + m.modLog.Error(err) + } + if res { m.CommandCounter.Total++ wg.Add(1) - go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore) + go m.getDynamoDBTablesPerRegion(r, wg, semaphore) + } + + res, err = servicemap.IsServiceInRegion("ec2", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getEc2InstancesPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getEc2ImagesPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getEc2SnapshotsPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getEc2VolumesPerRegion(r, wg, semaphore) + } + + res, err = servicemap.IsServiceInRegion("ecs", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getEcsTasksPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getEcsClustersPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getEcsServicesPerRegion(r, wg, semaphore) + + } + + res, err = servicemap.IsServiceInRegion("ecr", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getEcrRepositoriesPerRegion(r, wg, semaphore) + } + + res, err = servicemap.IsServiceInRegion("eks", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getEksClustersPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getEKSNodeGroupsPerRegion(r, wg, semaphore) } res, err = servicemap.IsServiceInRegion("elb", r) @@ -488,14 +608,36 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore go m.getELBListenersPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("mq", r) + res, err = servicemap.IsServiceInRegion("elasticache", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - m.getMqBrokersPerRegion(r, wg, semaphore) + go m.getElasticacheClustersPerRegion(r, wg, semaphore) + } + + res, err = servicemap.IsServiceInRegion("elasticbeanstalk", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getElasticBeanstalkApplicationsPerRegion(r, wg, semaphore) + } + + res, err = servicemap.IsServiceInRegion("elasticbeanstalk", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getEMRClustersPerRegion(r, wg, semaphore) + wg.Add(1) + go m.GetEMRInstancesPerRegion(r, wg, semaphore) } res, err = servicemap.IsServiceInRegion("es", r) @@ -518,10 +660,43 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore go m.getGrafanaWorkspacesPerRegion(r, wg, semaphore) } - // AppRunner is not supported in the aws service region catalog so we have to run it in all regions - m.CommandCounter.Total++ - wg.Add(1) - go m.getAppRunnerServicesPerRegion(r, wg, semaphore) + res, err = servicemap.IsServiceInRegion("glue", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getGlueDevEndpointsPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getGlueJobsPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getGlueTablesPerRegion(r, wg, semaphore) + wg.Add(1) + go m.getGlueDatabasesPerRegion(r, wg, semaphore) + + } + + res, err = servicemap.IsServiceInRegion("kinesis", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + go m.getKinesisDatastreamsPerRegion(r, wg, semaphore) + } + + res, err = servicemap.IsServiceInRegion("lambda", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + + wg.Add(1) + go m.getLambdaFunctionsPerRegion(r, wg, semaphore) + } res, err = servicemap.IsServiceInRegion("lightsail", r) if err != nil { @@ -533,71 +708,76 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore go m.getLightsailInstancesAndContainersPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("ssm", r) + res, err = servicemap.IsServiceInRegion("mq", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getSSMParametersPerRegion(r, wg, semaphore) + m.getMqBrokersPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("glue", r) + res, err = servicemap.IsServiceInRegion("rds", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getGlueDevEndpointsPerRegion(r, wg, semaphore) + go m.getRdsClustersPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("ssm", r) + res, err = servicemap.IsServiceInRegion("redshift", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getGlueJobsPerRegion(r, wg, semaphore) + go m.getRedshiftClustersPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("sns", r) + + res, err = servicemap.IsServiceInRegion("secretsmanager", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getSNSTopicsPerRegion(r, wg, semaphore) + go m.getSecretsManagerSecretsPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("sqs", r) + + res, err = servicemap.IsServiceInRegion("sns", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getSQSQueuesPerRegion(r, wg, semaphore) + go m.getSNSTopicsPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("dynamodb", r) + + res, err = servicemap.IsServiceInRegion("sqs", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getDynamoDBTablesPerRegion(r, wg, semaphore) + go m.getSQSQueuesPerRegion(r, wg, semaphore) } - res, err = servicemap.IsServiceInRegion("codebuild", r) + + res, err = servicemap.IsServiceInRegion("ssm", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ wg.Add(1) - go m.getCodeBuildProjectsPerRegion(r, wg, semaphore) + go m.getSSMParametersPerRegion(r, wg, semaphore) } + res, err = servicemap.IsServiceInRegion("stepfunctions", r) if err != nil { m.modLog.Error(err) @@ -610,6 +790,20 @@ func (m *Inventory2Module) executeChecks(r string, wg *sync.WaitGroup, semaphore } +func (m *Inventory2Module) PrintTotalResources(AWSOutputType string) { + var totalResources int + for _, r := range m.AWSRegions { + if m.totalRegionCounts[r] != 0 { + totalResources = totalResources + m.totalRegionCounts[r] + } + } + + for i := range m.GlobalResourceCounts { + totalResources = totalResources + m.GlobalResourceCounts[i].count + } + fmt.Printf("[%s][%s] %d resources found in the services we looked at. This is NOT the total number of resources in the account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), totalResources) +} + func (m *Inventory2Module) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() @@ -651,7 +845,7 @@ func (m *Inventory2Module) getLambdaFunctionsPerRegion(r string, wg *sync.WaitGr } -func (m *Inventory2Module) getEc2InstancesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getAthenaDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -661,42 +855,96 @@ func (m *Inventory2Module) getEc2InstancesPerRegion(r string, wg *sync.WaitGroup defer func() { <-semaphore }() + // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ - // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. + var totalCountThisServiceThisRegion = 0 - var service = "EC2 Instances" + var service = "Athena Databases" var resourceNames []string - // used CachedDescribeInstancesInput to avoid the need to call DescribeInstancesInput - DescribeInstances, err := sdk.CachedEC2DescribeInstances(m.EC2Client, aws.ToString(m.Caller.Account), r) - + ListDataCatalogs, err := sdk.CachedAthenaListDataCatalogs(m.AthenaClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeInstances) + for _, dc := range ListDataCatalogs { - // Add this page of resources to the module's resource list + ListDatabases, err := sdk.CachedAthenaListDatabases(m.AthenaClient, aws.ToString(m.Caller.Account), r, aws.ToString(dc.CatalogName)) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - for _, instance := range DescribeInstances { - arn := "arn:aws:ec2:" + r + ":" + aws.ToString(m.Caller.Account) + ":instance/" + aws.ToString(instance.InstanceId) - resourceNames = append(resourceNames, arn) + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListDatabases) + + for _, d := range ListDatabases { + arn := "arn:aws:athena:" + r + ":" + aws.ToString(m.Caller.Account) + ":database/" + d + resourceNames = append(resourceNames, 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 m.mu.Unlock() + } -func (m *Inventory2Module) getEksClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +// func (m *Inventory2Module) getAthenaDataCatalogsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +// defer func() { +// wg.Done() +// m.CommandCounter.Executing-- +// m.CommandCounter.Complete++ +// }() +// semaphore <- struct{}{} +// defer func() { +// <-semaphore +// }() + +// // m.CommandCounter.Total++ +// m.CommandCounter.Pending-- +// m.CommandCounter.Executing++ + +// var totalCountThisServiceThisRegion = 0 +// var service = "Athena Data Catalogs" +// var resourceNames []string + +// ListDataCatalogs, err := sdk.CachedAthenaListDataCatalogs(m.AthenaClient, aws.ToString(m.Caller.Account), r) +// if err != nil { +// m.modLog.Error(err.Error()) +// m.CommandCounter.Error++ +// return +// } + +// // Add this page of resources to the total count +// totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListDataCatalogs) + +// // Add this page of resources to the module's resource list +// for _, d := range ListDataCatalogs { +// arn := "arn:aws:athena:" + r + ":" + aws.ToString(m.Caller.Account) + ":datacatalog/" + aws.ToString(d.CatalogName) +// resourceNames = append(resourceNames, 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 +// m.mu.Unlock() +// } + +func (m *Inventory2Module) getEc2InstancesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -711,10 +959,12 @@ func (m *Inventory2Module) getEksClustersPerRegion(r string, wg *sync.WaitGroup, m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "EKS Clusters" + var service = "EC2 Instances" var resourceNames []string - ListClusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r) + // used CachedDescribeInstancesInput to avoid the need to call DescribeInstancesInput + DescribeInstances, err := sdk.CachedEC2DescribeInstances(m.EC2Client, aws.ToString(m.Caller.Account), r) + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -722,11 +972,12 @@ func (m *Inventory2Module) getEksClustersPerRegion(r string, wg *sync.WaitGroup, } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListClusters) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeInstances) // Add this page of resources to the module's resource list - for _, cluster := range ListClusters { - arn := "arn:aws:eks:" + r + ":" + aws.ToString(m.Caller.Account) + ":cluster/" + cluster + + for _, instance := range DescribeInstances { + arn := "arn:aws:ec2:" + r + ":" + aws.ToString(m.Caller.Account) + ":instance/" + aws.ToString(instance.InstanceId) resourceNames = append(resourceNames, arn) } @@ -736,10 +987,9 @@ func (m *Inventory2Module) getEksClustersPerRegion(r string, wg *sync.WaitGroup, m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion m.mu.Unlock() - } -func (m *Inventory2Module) getCloudFormationStacksPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getEc2ImagesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -754,10 +1004,11 @@ func (m *Inventory2Module) getCloudFormationStacksPerRegion(r string, wg *sync.W m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "CloudFormation Stacks" + var service = "EC2 AMIs" var resourceNames []string - ListStacks, err := sdk.CachedCloudFormationListStacks(m.CloudFormationClient, aws.ToString(m.Caller.Account), r) + // used CachedDescribeImagesInput to avoid the need to call DescribeImagesInput + DescribeImages, err := sdk.CachedEC2DescribeImages(m.EC2Client, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) @@ -766,13 +1017,12 @@ func (m *Inventory2Module) getCloudFormationStacksPerRegion(r string, wg *sync.W } // Add this page of resources to the total count - // Currently this counts both active and deleted stacks as they technically still exist. Might - // change this to only count active ones in the future. - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListStacks) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeImages) // Add this page of resources to the module's resource list - for _, stack := range ListStacks { - resourceNames = append(resourceNames, aws.ToString(stack.StackId)) + for _, image := range DescribeImages { + arn := "arn:aws:ec2:" + r + ":" + aws.ToString(m.Caller.Account) + ":image/" + aws.ToString(image.ImageId) + resourceNames = append(resourceNames, arn) } m.mu.Lock() @@ -781,10 +1031,9 @@ func (m *Inventory2Module) getCloudFormationStacksPerRegion(r string, wg *sync.W m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion m.mu.Unlock() - } -func (m *Inventory2Module) getSecretsManagerSecretsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getEc2SnapshotsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -799,10 +1048,11 @@ func (m *Inventory2Module) getSecretsManagerSecretsPerRegion(r string, wg *sync. m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "SecretsManager Secrets" + var service = "EC2 Snapshots" var resourceNames []string - ListSecrets, err := sdk.CachedSecretsManagerListSecrets(m.SecretsManagerClient, aws.ToString(m.Caller.Account), r) + // used CachedDescribeSnapshotsInput to avoid the need to call DescribeSnapshotsInput + DescribeSnapshots, err := sdk.CachedEC2DescribeSnapshots(m.EC2Client, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) @@ -810,25 +1060,24 @@ func (m *Inventory2Module) getSecretsManagerSecretsPerRegion(r string, wg *sync. return } - // Add this page of results to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListSecrets) + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeSnapshots) // Add this page of resources to the module's resource list - for _, secret := range ListSecrets { - resourceNames = append(resourceNames, aws.ToString(secret.ARN)) + for _, snapshot := range DescribeSnapshots { + arn := "arn:aws:ec2:" + r + ":" + aws.ToString(m.Caller.Account) + ":snapshot/" + aws.ToString(snapshot.SnapshotId) + 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() - } -func (m *Inventory2Module) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getEc2VolumesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -843,10 +1092,12 @@ func (m *Inventory2Module) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "RDS DB Instances" + var service = "EC2 Volumes" var resourceNames []string - DescribeDBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r) + // used CachedDescribeVolumesInput to avoid the need to call DescribeVolumesInput + DescribeVolumes, err := sdk.CachedEC2DescribeVolumes(m.EC2Client, aws.ToString(m.Caller.Account), r) + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -854,11 +1105,12 @@ func (m *Inventory2Module) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeDBInstances) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeVolumes) // Add this page of resources to the module's resource list - for _, instance := range DescribeDBInstances { - resourceNames = append(resourceNames, aws.ToString(instance.DBInstanceArn)) + for _, volume := range DescribeVolumes { + arn := "arn:aws:ec2:" + r + ":" + aws.ToString(m.Caller.Account) + ":volume/" + aws.ToString(volume.VolumeId) + resourceNames = append(resourceNames, arn) } m.mu.Lock() @@ -869,7 +1121,7 @@ func (m *Inventory2Module) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, m.mu.Unlock() } -func (m *Inventory2Module) getAPIGatewayvAPIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getEksClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -884,11 +1136,10 @@ func (m *Inventory2Module) getAPIGatewayvAPIsPerRegion(r string, wg *sync.WaitGr m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "APIGateway RestAPIs" + var service = "EKS Clusters" var resourceNames []string - GetRestApis, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r) - + ListClusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -896,11 +1147,11 @@ func (m *Inventory2Module) getAPIGatewayvAPIsPerRegion(r string, wg *sync.WaitGr } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(GetRestApis) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListClusters) // Add this page of resources to the module's resource list - for _, restAPI := range GetRestApis { - arn := aws.ToString(restAPI.Id) + for _, cluster := range ListClusters { + arn := "arn:aws:eks:" + r + ":" + aws.ToString(m.Caller.Account) + ":cluster/" + cluster resourceNames = append(resourceNames, arn) } @@ -910,9 +1161,10 @@ func (m *Inventory2Module) getAPIGatewayvAPIsPerRegion(r string, wg *sync.WaitGr m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion m.mu.Unlock() + } -func (m *Inventory2Module) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getEKSNodeGroupsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -927,35 +1179,43 @@ func (m *Inventory2Module) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitG m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "APIGatewayv2 APIs" + var service = "EKS Cluster NodeGroups" var resourceNames []string - GetApis, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r) - + ListClusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(GetApis) + for _, cluster := range ListClusters { + NodeGroups, err := sdk.CachedEKSListNodeGroups(m.EKSClient, aws.ToString(m.Caller.Account), r, cluster) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(NodeGroups) - // Add this page of resources to the module's resource list - for _, api := range GetApis { - arn := aws.ToString(api.ApiId) - resourceNames = append(resourceNames, arn) + // Add this page of resources to the module's resource list + for _, nodegroup := range NodeGroups { + arn := "arn:aws:eks:" + r + ":" + aws.ToString(m.Caller.Account) + ":nodegroup/" + cluster + "/" + nodegroup + resourceNames = append(resourceNames, 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 + m.mu.Unlock() } - 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() } -func (m *Inventory2Module) getELBv2ListenersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getCloudFormationStacksPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -965,13 +1225,16 @@ func (m *Inventory2Module) getELBv2ListenersPerRegion(r string, wg *sync.WaitGro defer func() { <-semaphore }() + // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ + // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "ELBv2 Load Balancers" + var service = "CloudFormation Stacks" var resourceNames []string - DescribeLoadBalancers, err := sdk.CachedELBv2DescribeLoadBalancers(m.ELBv2Client, aws.ToString(m.Caller.Account), r) + ListStacks, err := sdk.CachedCloudFormationListStacks(m.CloudFormationClient, aws.ToString(m.Caller.Account), r) + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -979,12 +1242,13 @@ func (m *Inventory2Module) getELBv2ListenersPerRegion(r string, wg *sync.WaitGro } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeLoadBalancers) + // Currently this counts both active and deleted stacks as they technically still exist. Might + // change this to only count active ones in the future. + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListStacks) // Add this page of resources to the module's resource list - for _, loadBalancer := range DescribeLoadBalancers { - arn := aws.ToString(loadBalancer.LoadBalancerArn) - resourceNames = append(resourceNames, arn) + for _, stack := range ListStacks { + resourceNames = append(resourceNames, aws.ToString(stack.StackId)) } m.mu.Lock() @@ -993,9 +1257,10 @@ func (m *Inventory2Module) getELBv2ListenersPerRegion(r string, wg *sync.WaitGro m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion m.mu.Unlock() + } -func (m *Inventory2Module) getELBListenersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getElasticacheClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1010,10 +1275,10 @@ func (m *Inventory2Module) getELBListenersPerRegion(r string, wg *sync.WaitGroup m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "ELB Load Balancers" + var service = "Elasticache Clusters" var resourceNames []string - DescribeLoadBalancers, err := sdk.CachedELBDescribeLoadBalancers(m.ELBClient, aws.ToString(m.Caller.Account), r) + ListClusters, err := sdk.CachedElastiCacheDescribeCacheClusters(m.ElasticacheClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) @@ -1022,12 +1287,11 @@ func (m *Inventory2Module) getELBListenersPerRegion(r string, wg *sync.WaitGroup } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeLoadBalancers) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListClusters) // Add this page of resources to the module's resource list - for _, loadBalancer := range DescribeLoadBalancers { - arn := "arn:aws:elasticloadbalancing:" + r + ":" + aws.ToString(m.Caller.Account) + ":loadbalancer/" + aws.ToString(loadBalancer.LoadBalancerName) - resourceNames = append(resourceNames, arn) + for _, cluster := range ListClusters { + resourceNames = append(resourceNames, aws.ToString(cluster.ARN)) } m.mu.Lock() @@ -1036,9 +1300,10 @@ func (m *Inventory2Module) getELBListenersPerRegion(r string, wg *sync.WaitGroup m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion m.mu.Unlock() + } -func (m *Inventory2Module) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getElasticBeanstalkApplicationsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1048,14 +1313,16 @@ func (m *Inventory2Module) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, s defer func() { <-semaphore }() - + // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ + // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "MQ Brokers" + var service = "ElasticBeanstalk Applications" var resourceNames []string - ListBrokers, err := sdk.CachedMQListBrokers(m.MQClient, aws.ToString(m.Caller.Account), r) + ListApplications, err := sdk.CachedElasticBeanstalkDescribeApplications(m.ElasticBeanstalkClient, aws.ToString(m.Caller.Account), r) + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -1063,11 +1330,12 @@ func (m *Inventory2Module) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, s } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListBrokers) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListApplications) // Add this page of resources to the module's resource list - for _, broker := range ListBrokers { - resourceNames = append(resourceNames, aws.ToString(broker.BrokerArn)) + for _, application := range ListApplications { + arn := aws.ToString(application.ApplicationArn) + resourceNames = append(resourceNames, arn) } m.mu.Lock() @@ -1076,9 +1344,10 @@ func (m *Inventory2Module) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, s m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion m.mu.Unlock() + } -func (m *Inventory2Module) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getEMRClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1093,10 +1362,11 @@ func (m *Inventory2Module) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "OpenSearch DomainNames" + var service = "EMR Clusters" var resourceNames []string - ListDomainNames, err := sdk.CachedOpenSearchListDomainNames(m.OpenSearchClient, aws.ToString(m.Caller.Account), r) + ListClusters, err := sdk.CachedEMRListClusters(m.EMRClient, aws.ToString(m.Caller.Account), r) + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -1104,12 +1374,11 @@ func (m *Inventory2Module) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListDomainNames) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListClusters) // Add this page of resources to the module's resource list - for _, domain := range ListDomainNames { - arn := "arn:aws:opensearch:" + r + ":" + aws.ToString(m.Caller.Account) + ":domain/" + aws.ToString(domain.DomainName) - resourceNames = append(resourceNames, arn) + for _, cluster := range ListClusters { + resourceNames = append(resourceNames, aws.ToString(cluster.ClusterArn)) } m.mu.Lock() @@ -1121,7 +1390,7 @@ func (m *Inventory2Module) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, } -func (m *Inventory2Module) getGrafanaWorkspacesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) GetEMRInstancesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1134,13 +1403,12 @@ func (m *Inventory2Module) getGrafanaWorkspacesPerRegion(r string, wg *sync.Wait // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ - // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. + var totalCountThisServiceThisRegion = 0 - var service = "Grafana Workspaces" + var service = "EMR Instances" var resourceNames []string - ListWorkspaces, err := sdk.CachedGrafanaListWorkspaces(m.GrafanaClient, aws.ToString(m.Caller.Account), r) - // 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). + ListClusters, err := sdk.CachedEMRListClusters(m.EMRClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) @@ -1148,13 +1416,24 @@ func (m *Inventory2Module) getGrafanaWorkspacesPerRegion(r string, wg *sync.Wait return } - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListWorkspaces) + for _, cluster := range ListClusters { - // Add this page of resources to the module's resource list - for _, workspace := range ListWorkspaces { - arn := "arn:aws:grafana:" + r + ":" + aws.ToString(m.Caller.Account) + ":workspace/" + aws.ToString(workspace.Id) - resourceNames = append(resourceNames, arn) + ListInstances, err := sdk.CachedEMRListInstances(m.EMRClient, aws.ToString(m.Caller.Account), r, aws.ToString(cluster.Id)) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListInstances) + + // Add this page of resources to the module's resource list + for _, instance := range ListInstances { + arn := "arn:aws:elasticmapreduce:" + r + ":" + aws.ToString(m.Caller.Account) + ":instance/" + aws.ToString(instance.Id) + resourceNames = append(resourceNames, arn) + } } m.mu.Lock() @@ -1165,7 +1444,7 @@ func (m *Inventory2Module) getGrafanaWorkspacesPerRegion(r string, wg *sync.Wait m.mu.Unlock() } -func (m *Inventory2Module) getAppRunnerServicesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getSecretsManagerSecretsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1178,37 +1457,38 @@ func (m *Inventory2Module) getAppRunnerServicesPerRegion(r string, wg *sync.Wait // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ - + // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "AppRunner Services" + var service = "SecretsManager Secrets" var resourceNames []string - ServiceSummaryList, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r) + ListSecrets, err := sdk.CachedSecretsManagerListSecrets(m.SecretsManagerClient, aws.ToString(m.Caller.Account), r) if err != nil { - //modLog.Error(err.Error()) m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ServiceSummaryList) + // Add this page of results to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListSecrets) // Add this page of resources to the module's resource list - for _, service := range ServiceSummaryList { - resourceNames = append(resourceNames, aws.ToString(service.ServiceArn)) + for _, secret := range ListSecrets { + resourceNames = append(resourceNames, aws.ToString(secret.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() + } -func (m *Inventory2Module) getLightsailInstancesAndContainersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1223,27 +1503,10 @@ func (m *Inventory2Module) getLightsailInstancesAndContainersPerRegion(r string, m.CommandCounter.Executing++ // "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 service = "RDS DB Instances" var resourceNames []string - ContainerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r) - - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - return - } else { - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ContainerServices) - - // Add this page of resources to the module's resource list - for _, containerService := range ContainerServices { - resourceNames = append(resourceNames, aws.ToString(containerService.Arn)) - } - } - - Instances, err := sdk.CachedLightsailGetInstances(m.LightsailClient, aws.ToString(m.Caller.Account), r) - + DescribeDBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -1251,11 +1514,11 @@ func (m *Inventory2Module) getLightsailInstancesAndContainersPerRegion(r string, } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Instances) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeDBInstances) // Add this page of resources to the module's resource list - for _, instance := range Instances { - resourceNames = append(resourceNames, aws.ToString(instance.Arn)) + for _, instance := range DescribeDBInstances { + resourceNames = append(resourceNames, aws.ToString(instance.DBInstanceArn)) } m.mu.Lock() @@ -1264,10 +1527,9 @@ func (m *Inventory2Module) getLightsailInstancesAndContainersPerRegion(r string, m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion m.mu.Unlock() - } -func (m *Inventory2Module) getSSMParametersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getAPIGatewayvAPIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1281,54 +1543,36 @@ func (m *Inventory2Module) getSSMParametersPerRegion(r string, wg *sync.WaitGrou m.CommandCounter.Pending-- m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string var totalCountThisServiceThisRegion = 0 - var service = "SSM Parameters" + var service = "APIGateway RestAPIs" var resourceNames []string - for { - DescribeParameters, err := m.SSMClient.DescribeParameters( - context.TODO(), - &(ssm.DescribeParametersInput{ - NextToken: PaginationControl, - }), - func(o *ssm.Options) { - o.Region = r - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeParameters.Parameters) + GetRestApis, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r) - // 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) - } + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - // Pagination control. After the last page of output, the for loop exits. - if 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 - m.mu.Unlock() - break - } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(GetRestApis) + // Add this page of resources to the module's resource list + for _, restAPI := range GetRestApis { + arn := aws.ToString(restAPI.Id) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() } -func (m *Inventory2Module) getEcsTasksPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1342,78 +1586,36 @@ func (m *Inventory2Module) getEcsTasksPerRegion(r string, wg *sync.WaitGroup, se m.CommandCounter.Pending-- m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string - var PaginationControl2 *string var totalCountThisServiceThisRegion = 0 - var service = "ECS Tasks" + var service = "APIGatewayv2 APIs" var resourceNames []string - for { - ListClusters, err := m.ECSClient.ListClusters( - context.TODO(), - &(ecs.ListClustersInput{ - NextToken: PaginationControl, - }), - func(o *ecs.Options) { - o.Region = r - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - for _, cluster := range ListClusters.ClusterArns { - ListTasks, err := m.ECSClient.ListTasks( - context.TODO(), - &(ecs.ListTasksInput{ - Cluster: &cluster, - NextToken: PaginationControl2, - }), - func(o *ecs.Options) { - o.Region = r - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListTasks.TaskArns) - - // Add this page of resources to the module's resource list - for _, task := range ListTasks.TaskArns { - resourceNames = append(resourceNames, task) - } + GetApis, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r) - if ListTasks.NextToken != nil { - PaginationControl2 = ListTasks.NextToken - } else { - PaginationControl2 = nil - break - } - } + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - // 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 - m.mu.Unlock() - break - } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(GetApis) + + // Add this page of resources to the module's resource list + for _, api := range GetApis { + arn := aws.ToString(api.ApiId) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() } -func (m *Inventory2Module) getGlueDevEndpointsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { - // Don't use this method as a template for future ones. There is a one off in the way the NextToken is handled. +func (m *Inventory2Module) getELBv2ListenersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1423,57 +1625,37 @@ func (m *Inventory2Module) getGlueDevEndpointsPerRegion(r string, wg *sync.WaitG defer func() { <-semaphore }() - // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ var totalCountThisServiceThisRegion = 0 - var service = "Glue Dev Endpoints" - var PaginationControl *string + var service = "ELBv2 Load Balancers" var resourceNames []string - for { - ListDevEndpoints, err := m.GlueClient.ListDevEndpoints( - context.TODO(), - &(glue.ListDevEndpointsInput{ - NextToken: PaginationControl, - }), - func(o *glue.Options) { - o.Region = r - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListDevEndpoints.DevEndpointNames) + DescribeLoadBalancers, err := sdk.CachedELBv2DescribeLoadBalancers(m.ELBv2Client, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - // 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) - } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeLoadBalancers) - // This next line is non-standard. For some reason this next token is an empty string instead of nil, so - // as a result we had to change the comparison. - if aws.ToString(ListDevEndpoints.NextToken) != "" { - PaginationControl = ListDevEndpoints.NextToken - } else { - PaginationControl = nil - m.mu.Lock() - m.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() - break - } + // Add this page of resources to the module's resource list + for _, loadBalancer := range DescribeLoadBalancers { + arn := aws.ToString(loadBalancer.LoadBalancerArn) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() } -func (m *Inventory2Module) getGlueJobsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getELBListenersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1487,55 +1669,36 @@ func (m *Inventory2Module) getGlueJobsPerRegion(r string, wg *sync.WaitGroup, se m.CommandCounter.Pending-- m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string var totalCountThisServiceThisRegion = 0 - var service = "Glue Jobs" + var service = "ELB Load Balancers" var resourceNames []string - for { - m.modLog.Info(fmt.Sprintf("Getting jobs %v\n", PaginationControl)) - ListJobs, err := m.GlueClient.ListJobs( - context.TODO(), - &(glue.ListJobsInput{ - NextToken: PaginationControl, - }), - func(o *glue.Options) { - o.Region = r - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } + DescribeLoadBalancers, err := sdk.CachedELBDescribeLoadBalancers(m.ELBClient, aws.ToString(m.Caller.Account), r) - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListJobs.JobNames) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - // 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) - } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DescribeLoadBalancers) - // The "NextToken" value is nil when there's no more data to return. - if ListJobs.NextToken != nil { - PaginationControl = ListJobs.NextToken - } else { - PaginationControl = nil - // No more pages, update the module's service map - m.mu.Lock() - m.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() - break - } + // Add this page of resources to the module's resource list + for _, loadBalancer := range DescribeLoadBalancers { + arn := "arn:aws:elasticloadbalancing:" + r + ":" + aws.ToString(m.Caller.Account) + ":loadbalancer/" + aws.ToString(loadBalancer.LoadBalancerName) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() } -func (m *Inventory2Module) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getMqBrokersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1545,57 +1708,37 @@ func (m *Inventory2Module) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, s defer func() { <-semaphore }() - // m.CommandCounter.Total++ + m.CommandCounter.Pending-- m.CommandCounter.Executing++ - // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string var totalCountThisServiceThisRegion = 0 - var service = "SNS Topics" + var service = "MQ Brokers" var resourceNames []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 - } - - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListTopics.Topics) + ListBrokers, err := sdk.CachedMQListBrokers(m.MQClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - // Add this page of resources to the module's resource list - for _, topic := range ListTopics.Topics { - resourceNames = append(resourceNames, aws.ToString(topic.TopicArn)) - } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListBrokers) - // The "NextToken" value is nil when there's no more data to return. - if ListTopics.NextToken != nil { - PaginationControl = ListTopics.NextToken - } else { - PaginationControl = nil - // No more pages, update the module's service map - m.mu.Lock() - m.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() - break - } + // Add this page of resources to the module's resource list + for _, broker := range ListBrokers { + resourceNames = append(resourceNames, aws.ToString(broker.BrokerArn)) } + + 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() } -func (m *Inventory2Module) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getOpenSearchPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1609,54 +1752,36 @@ func (m *Inventory2Module) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, s m.CommandCounter.Pending-- m.CommandCounter.Executing++ // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string var totalCountThisServiceThisRegion = 0 - var service = "SQS Queues" + var service = "OpenSearch DomainNames" var resourceNames []string - for { - ListQueues, err := m.SQSClient.ListQueues( - context.TODO(), - &(sqs.ListQueuesInput{ - NextToken: PaginationControl, - }), - func(o *sqs.Options) { - o.Region = r - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - - // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListQueues.QueueUrls) + ListDomainNames, err := sdk.CachedOpenSearchListDomainNames(m.OpenSearchClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - // 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) - } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListDomainNames) - // The "NextToken" value is nil when there's no more data to return. - if ListQueues.NextToken != nil { - PaginationControl = ListQueues.NextToken - } else { - PaginationControl = nil - // No more pages, update the module's service map - m.mu.Lock() - m.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() - break - } + // Add this page of resources to the module's resource list + for _, domain := range ListDomainNames { + arn := "arn:aws:opensearch:" + r + ":" + aws.ToString(m.Caller.Account) + ":domain/" + aws.ToString(domain.DomainName) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() + } -func (m *Inventory2Module) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getGrafanaWorkspacesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1669,11 +1794,14 @@ func (m *Inventory2Module) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGro // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ + // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "DynamoDB Tables" + var service = "Grafana Workspaces" var resourceNames []string - TableNames, err := sdk.CachedDynamoDBListTables(m.DynamoDBClient, aws.ToString(m.Caller.Account), r) + ListWorkspaces, err := sdk.CachedGrafanaListWorkspaces(m.GrafanaClient, aws.ToString(m.Caller.Account), r) + // 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). + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -1681,25 +1809,23 @@ func (m *Inventory2Module) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGro } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(TableNames) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListWorkspaces) // Add this page of resources to the module's resource list - for _, table := range TableNames { - arn := "arn:aws:dynamodb:" + r + ":" + aws.ToString(m.Caller.Account) + ":table/" + table + for _, workspace := range ListWorkspaces { + arn := "arn:aws:grafana:" + r + ":" + aws.ToString(m.Caller.Account) + ":workspace/" + aws.ToString(workspace.Id) 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() } -func (m *Inventory2Module) getCodeBuildProjectsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getAppRunnerServicesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1712,37 +1838,37 @@ func (m *Inventory2Module) getCodeBuildProjectsPerRegion(r string, wg *sync.Wait // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 - var service = "CodeBuild Projects" + var service = "AppRunner Services" var resourceNames []string - projects, err := m.getcodeBuildProjects(r) + ServiceSummaryList, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r) + if err != nil { + //modLog.Error(err.Error()) m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(projects) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ServiceSummaryList) // Add this page of resources to the module's resource list - for _, project := range projects { - arn := "arn:aws:codebuild:" + r + ":" + aws.ToString(m.Caller.Account) + ":project/" + project - resourceNames = append(resourceNames, arn) + for _, service := range ServiceSummaryList { + resourceNames = append(resourceNames, aws.ToString(service.ServiceArn)) } - // 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() } -func (m *Inventory2Module) getStepFunctionsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { +func (m *Inventory2Module) getLightsailInstancesAndContainersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { defer func() { wg.Done() m.CommandCounter.Executing-- @@ -1755,11 +1881,28 @@ func (m *Inventory2Module) getStepFunctionsPerRegion(r string, wg *sync.WaitGrou // m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ + // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. var totalCountThisServiceThisRegion = 0 - var service = "StepFunctions State Machines" + var service = "Lightsail Instances/Containers" var resourceNames []string - ListStateMachines, err := sdk.CachedStepFunctionsListStateMachines(m.StepFunctionClient, aws.ToString(m.Caller.Account), r) + ContainerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } else { + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ContainerServices) + + // Add this page of resources to the module's resource list + for _, containerService := range ContainerServices { + resourceNames = append(resourceNames, aws.ToString(containerService.Arn)) + } + } + + Instances, err := sdk.CachedLightsailGetInstances(m.LightsailClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) @@ -1768,12 +1911,11 @@ func (m *Inventory2Module) getStepFunctionsPerRegion(r string, wg *sync.WaitGrou } // Add this page of resources to the total count - totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListStateMachines) + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Instances) // Add this page of resources to the module's resource list - for _, stateMachine := range ListStateMachines { - arn := "arn:aws:states:" + r + ":" + aws.ToString(m.Caller.Account) + ":stateMachine:" + aws.ToString(stateMachine.Name) - resourceNames = append(resourceNames, arn) + for _, instance := range Instances { + resourceNames = append(resourceNames, aws.ToString(instance.Arn)) } m.mu.Lock() @@ -1782,41 +1924,1024 @@ func (m *Inventory2Module) getStepFunctionsPerRegion(r string, wg *sync.WaitGrou m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion m.mu.Unlock() -} -// Global Resources +} -func (m *Inventory2Module) getBuckets(verbosity int, dataReceiver chan GlobalResourceCount2) { - // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var total int - resourceType := "S3 Buckets" +func (m *Inventory2Module) getSSMParametersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "SSM Parameters" var resourceNames []string - ListBuckets, err := m.S3Client.ListBuckets( - context.TODO(), - &s3.ListBucketsInput{}, - ) + Parameters, err := sdk.CachedSSMDescribeParameters(m.SSMClient, aws.ToString(m.Caller.Account), r) + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } - total = len(ListBuckets.Buckets) + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Parameters) // 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) + for _, parameter := range Parameters { + arn := "arn:aws:ssm:" + r + ":" + aws.ToString(m.Caller.Account) + ":parameter/" + aws.ToString(parameter.Name) + resourceNames = append(resourceNames, arn) } - dataReceiver <- GlobalResourceCount2{ + 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() + +} + +func (m *Inventory2Module) getEcsTasksPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + + var totalCountThisServiceThisRegion = 0 + var service = "ECS Tasks" + var resourceNames []string + + Clusters, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + for _, cluster := range Clusters { + + Tasks, err := sdk.CachedECSListTasks(m.ECSClient, aws.ToString(m.Caller.Account), r, cluster) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Tasks) + + // Add this page of resources to the module's resource list + for _, task := range Tasks { + resourceNames = append(resourceNames, task) + } + + } + + 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() + +} + +func (m *Inventory2Module) getEcsServicesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + + var totalCountThisServiceThisRegion = 0 + var service = "ECS Services" + var resourceNames []string + + Clusters, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + for _, cluster := range Clusters { + + Services, err := sdk.CachedECSListServices(m.ECSClient, aws.ToString(m.Caller.Account), r, cluster) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Services) + + // Add this page of resources to the module's resource list + for _, service := range Services { + arn := "arn:aws:ecs:" + r + ":" + aws.ToString(m.Caller.Account) + ":service/" + service + resourceNames = append(resourceNames, 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 + m.mu.Unlock() + +} + +func (m *Inventory2Module) getEcsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + + var totalCountThisServiceThisRegion = 0 + var service = "ECS Clusters" + var resourceNames []string + + Clusters, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Clusters) + + // Add this page of resources to the module's resource list + for _, cluster := range Clusters { + resourceNames = append(resourceNames, cluster) + } + + 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() + +} + +func (m *Inventory2Module) getEcrRepositoriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + // Don't use this method as a template for future ones. There is a one off in the way the NextToken is handled. + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + + var totalCountThisServiceThisRegion = 0 + var service = "ECR Repositories" + var resourceNames []string + + Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Repositories) + + // Add this page of resources to the module's resource list + for _, repo := range Repositories { + resourceNames = append(resourceNames, aws.ToString(repo.RepositoryArn)) + } + + 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() + +} + +func (m *Inventory2Module) getGlueDevEndpointsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + // Don't use this method as a template for future ones. There is a one off in the way the NextToken is handled. + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "Glue Dev Endpoints" + var resourceNames []string + + DevEndpointNames, err := sdk.CachedGlueListDevEndpoints(m.GlueClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(DevEndpointNames) + + // Add this page of resources to the module's resource list + for _, devEndpoint := range DevEndpointNames { + arn := "arn:aws:glue:" + r + ":" + aws.ToString(m.Caller.Account) + ":devEndpoint/" + devEndpoint + resourceNames = append(resourceNames, 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 + m.mu.Unlock() + +} + +func (m *Inventory2Module) getGlueJobsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + + var totalCountThisServiceThisRegion = 0 + var service = "Glue Jobs" + var resourceNames []string + + JobNames, err := sdk.CachedGlueListJobs(m.GlueClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(JobNames) + + // Add this page of resources to the module's resource list + for _, job := range JobNames { + arn := "arn:aws:glue:" + r + ":" + aws.ToString(m.Caller.Account) + ":job/" + job + resourceNames = append(resourceNames, 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 + m.mu.Unlock() + +} + +func (m *Inventory2Module) getGlueDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + + var totalCountThisServiceThisRegion = 0 + var service = "Glue Databases" + var resourceNames []string + + Databases, err := sdk.CachedGlueGetDatabases(m.GlueClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + for _, database := range Databases { + arn := "arn:aws:glue:" + r + ":" + aws.ToString(m.Caller.Account) + ":database/" + aws.ToString(database.Name) + resourceNames = append(resourceNames, arn) + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Databases) + + // Add this page of resources to the module's resource list + for _, database := range Databases { + resourceNames = append(resourceNames, aws.ToString(database.Name)) + } + + 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() + +} + +func (m *Inventory2Module) getGlueTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + + var totalCountThisServiceThisRegion = 0 + var service = "Glue Jobs" + var resourceNames []string + + Databases, err := sdk.CachedGlueGetDatabases(m.GlueClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + for _, database := range Databases { + TableNames, err := sdk.CachedGlueGetTables(m.GlueClient, aws.ToString(m.Caller.Account), r, aws.ToString(database.Name)) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + + } + + // Add this page of resources to the total count + + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(TableNames) + + // Add this page of resources to the module's resource list + for _, table := range TableNames { + arn := "arn:aws:glue:" + r + ":" + aws.ToString(m.Caller.Account) + ":table/" + aws.ToString(database.Name) + "/" + aws.ToString(table.Name) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() +} + +func (m *Inventory2Module) getKinesisDatastreamsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + + var totalCountThisServiceThisRegion = 0 + var service = "Kinesis Data Streams" + var resourceNames []string + + Datastreams, err := sdk.CachedKinesisListStreams(m.KinesisClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Datastreams) + + // Add this page of resources to the module's resource list + for _, stream := range Datastreams { + arn := "arn:aws:kinesis:" + r + ":" + aws.ToString(m.Caller.Account) + ":stream/" + stream + resourceNames = append(resourceNames, 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 + m.mu.Unlock() + +} + +func (m *Inventory2Module) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. + var totalCountThisServiceThisRegion = 0 + var service = "SNS Topics" + var resourceNames []string + + Topics, err := sdk.CachedSNSListTopics(m.SNSClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Topics) + + // Add this page of resources to the module's resource list + + resourceNames = append(resourceNames, Topics...) + + 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() +} + +func (m *Inventory2Module) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "SQS Queues" + var resourceNames []string + + QueueUrls, err := sdk.CachedSQSListQueues(m.SQSClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(QueueUrls) + + // Add this page of resources to the module's resource list + for _, queue := range QueueUrls { + arn := "arn:aws:sqs:" + r + ":" + aws.ToString(m.Caller.Account) + ":" + queue + resourceNames = append(resourceNames, 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 + m.mu.Unlock() + +} + +func (m *Inventory2Module) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "DynamoDB Tables" + var resourceNames []string + + TableNames, err := sdk.CachedDynamoDBListTables(m.DynamoDBClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(TableNames) + + // Add this page of resources to the module's resource list + for _, table := range 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() +} + +func (m *Inventory2Module) getRedshiftClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "Redshift Clusters" + var resourceNames []string + + Clusters, err := sdk.CachedRedShiftDescribeClusters(m.RedshiftClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Clusters) + + // Add this page of resources to the module's resource list + for _, cluster := range Clusters { + //arn := "arn:aws:redshift:" + r + ":" + aws.ToString(m.Caller.Account) + ":cluster:" + cluster + + resourceNames = append(resourceNames, aws.ToString(cluster.ClusterNamespaceArn)) + } + + 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() +} + +func (m *Inventory2Module) getCodeArtifactDomainsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "CodeArtifact Domains" + var resourceNames []string + + Domains, err := sdk.CachedCodeArtifactListDomains(m.CodeArtifactClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Domains) + + // Add this page of resources to the module's resource list + for _, domain := range Domains { + arn := aws.ToString(domain.Arn) + 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() +} + +func (m *Inventory2Module) getCodeBuildProjectsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "CodeBuild Projects" + var resourceNames []string + + projects, err := sdk.CachedCodeBuildListProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(projects) + + // Add this page of resources to the module's resource list + for _, project := range projects { + arn := "arn:aws:codebuild:" + r + ":" + aws.ToString(m.Caller.Account) + ":project/" + project + 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() +} + +func (m *Inventory2Module) getCodeCommitRepositoriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "CodeCommit Repositories" + var resourceNames []string + + repos, err := sdk.CachedCodeCommitListRepositories(m.CodeCommitClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(repos) + + // Add this page of resources to the module's resource list + for _, repo := range repos { + arn := "arn:aws:codecommit:" + r + ":" + aws.ToString(m.Caller.Account) + ":" + aws.ToString(repo.RepositoryName) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() +} + +func (m *Inventory2Module) getCodeDeployApplicationsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "CodeDeploy Applications" + var resourceNames []string + + apps, err := sdk.CachedCodeDeployListApplications(m.CodeDeployClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(apps) + + // Add this page of resources to the module's resource list + for _, app := range apps { + arn := "arn:aws:codedeploy:" + r + ":" + aws.ToString(m.Caller.Account) + ":application:" + app + resourceNames = append(resourceNames, 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 + m.mu.Unlock() +} + +func (m *Inventory2Module) getCodeDeployDeploymentsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "CodeDeploy Deployments" + var resourceNames []string + + deployments, err := sdk.CachedCodeDeployListDeployments(m.CodeDeployClient, aws.ToString(m.Caller.Account), r) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(deployments) + + // Add this page of resources to the module's resource list + for _, d := range deployments { + arn := "arn:aws:codedeploy:" + r + ":" + aws.ToString(m.Caller.Account) + ":application:" + d + resourceNames = append(resourceNames, 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 + m.mu.Unlock() +} + +func (m *Inventory2Module) getDataPipelinePipelinesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "DataPipeline Pipelines" + var resourceNames []string + + pipelines, err := sdk.CachedDataPipelineListPipelines(m.DataPipelineClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(pipelines) + + // Add this page of resources to the module's resource list + for _, p := range pipelines { + arn := "arn:aws:datapipeline:" + r + ":" + aws.ToString(m.Caller.Account) + ":pipeline:" + aws.ToString(p.Id) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() +} + +func (m *Inventory2Module) getStepFunctionsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "StepFunctions State Machines" + var resourceNames []string + + ListStateMachines, err := sdk.CachedStepFunctionsListStateMachines(m.StepFunctionClient, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(ListStateMachines) + + // Add this page of resources to the module's resource list + for _, stateMachine := range ListStateMachines { + arn := "arn:aws:states:" + r + ":" + aws.ToString(m.Caller.Account) + ":stateMachine:" + aws.ToString(stateMachine.Name) + resourceNames = append(resourceNames, 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 + m.mu.Unlock() +} + +func (m *Inventory2Module) getCloud9EnvironmentsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}) { + defer func() { + wg.Done() + m.CommandCounter.Executing-- + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + m.CommandCounter.Total++ + m.CommandCounter.Pending-- + m.CommandCounter.Executing++ + var totalCountThisServiceThisRegion = 0 + var service = "Cloud9 Environments" + var resourceNames []string + + Environments, err := sdk.CachedCloud9ListEnvironments(m.Cloud9Client, aws.ToString(m.Caller.Account), r) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Environments) + + // Add this page of resources to the module's resource list + for _, env := range Environments { + arn := "arn:aws:cloud9:" + r + ":" + aws.ToString(m.Caller.Account) + ":environment:" + env + 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.mu.Unlock() +} + +// Global Resources + +func (m *Inventory2Module) getBuckets(verbosity int, dataReceiver chan GlobalResourceCount2) { + // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. + var total int + resourceType := "S3 Buckets" + service := "S3 Buckets" + var r string = "Global" + var totalCountThisServiceThisRegion = 0 + var resourceNames []string + + Buckets, err := sdk.CachedListBuckets(m.S3Client, aws.ToString(m.Caller.Account)) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + total = len(Buckets) + + // Add this page of resources to the module's resource list + for _, bucket := range Buckets { + arn := "arn:aws:s3:::" + aws.ToString(bucket.Name) + m.resources = append(m.resources, arn) + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Buckets) + + dataReceiver <- GlobalResourceCount2{ resourceType: resourceType, count: total, } 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() // if verbosity > 1 { // fmt.Printf("S3 Buckets: %d\n", total_buckets) @@ -1824,169 +2949,294 @@ func (m *Inventory2Module) getBuckets(verbosity int, dataReceiver chan GlobalRes } func (m *Inventory2Module) getCloudfrontDistros(verbosity int, dataReceiver chan GlobalResourceCount2) { - // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string var total int + var r string = "Global" + service := "Cloudfront Distributions" resourceType := "Cloudfront Distributions" + var totalCountThisServiceThisRegion = 0 var resourceNames []string + Items, err := sdk.CachedCloudFrontListDistributions(m.CloudfrontClient, aws.ToString(m.Caller.Account)) // 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 { - ListDistributions, err := m.CloudfrontClient.ListDistributions( - context.TODO(), - &cloudfront.ListDistributionsInput{ - Marker: PaginationControl, - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - if ListDistributions.DistributionList.Quantity == nil { - break - } - // Add this page of resources to the total count - total = total + len(ListDistributions.DistributionList.Items) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } - // Add this page of resources to the module's resource list - for _, distro := range ListDistributions.DistributionList.Items { - resourceNames = append(resourceNames, aws.ToString(distro.ARN)) - } + // Add this page of resources to the total count + total = total + len(Items) - // Pagination control. After the last page of output, the for loop exits. - if ListDistributions.DistributionList.NextMarker != nil { - PaginationControl = ListDistributions.DistributionList.NextMarker - } else { - PaginationControl = nil - dataReceiver <- GlobalResourceCount2{ - resourceType: resourceType, - count: total, - } - break - } + // Add this page of resources to the module's resource list + for _, distro := range Items { + resourceNames = append(resourceNames, aws.ToString(distro.ARN)) + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Items) + dataReceiver <- GlobalResourceCount2{ + resourceType: resourceType, + count: total, } 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() } func (m *Inventory2Module) getIAMUsers(verbosity int, dataReceiver chan GlobalResourceCount2) { - // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string + var total int + var r string = "Global" + service := "IAM Users" + var totalCountThisServiceThisRegion = 0 resourceType := "IAM Users" var resourceNames []string - for { - ListUsers, err := m.IAMClient.ListUsers( - context.TODO(), - &iam.ListUsersInput{ - Marker: PaginationControl, - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - total = total + len(ListUsers.Users) + Users, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account)) - // Add this page of resources to the module's resource list - for _, user := range ListUsers.Users { - resourceNames = append(resourceNames, aws.ToString(user.Arn)) - } + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + total = total + len(Users) - // Pagination control. After the last page of output, the for loop exits. - if ListUsers.Marker != nil { - PaginationControl = ListUsers.Marker - } else { - PaginationControl = nil - dataReceiver <- GlobalResourceCount2{ - resourceType: resourceType, - count: total, - } - break - } + // Add this page of resources to the module's resource list + for _, user := range Users { + resourceNames = append(resourceNames, aws.ToString(user.Arn)) + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Users) + + dataReceiver <- GlobalResourceCount2{ + resourceType: resourceType, + count: total, + } + 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() +} +func (m *Inventory2Module) getIAMRoles(verbosity int, dataReceiver chan GlobalResourceCount2) { + var total int + var r string = "Global" + service := "IAM Roles" + var totalCountThisServiceThisRegion = 0 + resourceType := "IAM Roles" + var resourceNames []string + + Roles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account)) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + total = total + len(Roles) + + // Add this page of resources to the module's resource list + for _, role := range Roles { + resourceNames = append(resourceNames, aws.ToString(role.Arn)) + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Roles) + + dataReceiver <- GlobalResourceCount2{ + resourceType: resourceType, + count: total, } + 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() } -func (m *Inventory2Module) getIAMRoles(verbosity int, dataReceiver chan GlobalResourceCount2) { - // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string +func (m *Inventory2Module) getIAMGroups(verbosity int, dataReceiver chan GlobalResourceCount2) { var total int - var resourceType = "IAM Roles" + var r string = "Global" + service := "IAM Groups" + var totalCountThisServiceThisRegion = 0 + resourceType := "IAM Groups" var resourceNames []string - for { - ListRoles, err := m.IAMClient.ListRoles( - context.TODO(), - &iam.ListRolesInput{ - Marker: PaginationControl, - }, - ) + Groups, err := sdk.CachedIamListGroups(m.IAMClient, aws.ToString(m.Caller.Account)) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + total = total + len(Groups) + + // Add this page of resources to the module's resource list + for _, group := range Groups { + resourceNames = append(resourceNames, aws.ToString(group.Arn)) + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Groups) + + dataReceiver <- GlobalResourceCount2{ + resourceType: resourceType, + count: total, + } + + 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() +} + +func (m *Inventory2Module) getIAMAccessKeys(verbosity int, dataReceiver chan GlobalResourceCount2) { + var total int + var r string = "Global" + service := "IAM Access Keys" + var totalCountThisServiceThisRegion = 0 + resourceType := "IAM Access Keys" + var resourceNames []string + + Users, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account)) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + for _, user := range Users { + + AccessKeys, err := sdk.CachedIamListAccessKeys(m.IAMClient, aws.ToString(m.Caller.Account), aws.ToString(user.UserName)) + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ - break + return } - total = total + len(ListRoles.Roles) + total = total + len(AccessKeys) // Add this page of resources to the module's resource list - for _, role := range ListRoles.Roles { - resourceNames = append(resourceNames, aws.ToString(role.Arn)) + for _, key := range AccessKeys { + resourceNames = append(resourceNames, aws.ToString(key.UserName)) } - // Pagination control. After the last page of output, the for loop exits. - if ListRoles.Marker != nil { - PaginationControl = ListRoles.Marker - } else { - PaginationControl = nil - dataReceiver <- GlobalResourceCount2{ - resourceType: resourceType, - count: total, - } - break + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(AccessKeys) + + dataReceiver <- GlobalResourceCount2{ + resourceType: resourceType, + count: total, } } + m.mu.Lock() m.resources = append(m.resources, resourceNames...) - m.mu.Unlock() + m.serviceMap[service][r] = totalCountThisServiceThisRegion + m.totalRegionCounts[r] = m.totalRegionCounts[r] + totalCountThisServiceThisRegion + m.serviceMap["total"][r] = m.serviceMap["total"][r] + totalCountThisServiceThisRegion + m.mu.Unlock() } -func (m *Inventory2Module) PrintTotalResources(outputFormat string) { - var totalResources int - for _, r := range m.AWSRegions { - if m.totalRegionCounts[r] != 0 { - totalResources = totalResources + m.totalRegionCounts[r] - } +func (m *Inventory2Module) getRoute53Zones(verbosity int, dataReceiver chan GlobalResourceCount2) { + var total int + var r string = "Global" + service := "Route53 Zones" + var totalCountThisServiceThisRegion = 0 + resourceType := "Route53 Zones" + var resourceNames []string + + Zones, err := sdk.CachedRoute53ListHostedZones(m.Route53Client, aws.ToString(m.Caller.Account)) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return } + total = total + len(Zones) - for i := range m.GlobalResourceCounts { - totalResources = totalResources + m.GlobalResourceCounts[i].count + // Add this page of resources to the module's resource list + for _, zone := range Zones { + resourceNames = append(resourceNames, aws.ToString(zone.Id)) } - fmt.Printf("[%s][%s] %d resources found in the services we looked at. This is NOT the total number of resources in the account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), totalResources) + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Zones) + + dataReceiver <- GlobalResourceCount2{ + resourceType: resourceType, + count: total, + } + + 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() } -func (m *Inventory2Module) getcodeBuildProjects(r string) ([]string, error) { - CodeBuildProjects, err := m.CodeBuildClient.ListProjects( - context.TODO(), - &codebuild.ListProjectsInput{}, - func(options *codebuild.Options) { - options.Region = r - }, - ) +func (m *Inventory2Module) getRoute53Records(verbosity int, dataReceiver chan GlobalResourceCount2) { + var total int + var r string = "Global" + service := "Route53 Records" + var totalCountThisServiceThisRegion = 0 + resourceType := "Route53 Records" + var resourceNames []string + + Zones, err := sdk.CachedRoute53ListHostedZones(m.Route53Client, aws.ToString(m.Caller.Account)) + if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ - return nil, err + return + } + + for _, zone := range Zones { + Records, err := sdk.CachedRoute53ListResourceRecordSets(m.Route53Client, aws.ToString(m.Caller.Account), aws.ToString(zone.Id)) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + total = total + len(Records) + + // Add this page of resources to the module's resource list + for _, record := range Records { + resourceNames = append(resourceNames, aws.ToString(record.Name)) + } + + // Add this page of resources to the total count + totalCountThisServiceThisRegion = totalCountThisServiceThisRegion + len(Records) + } + + dataReceiver <- GlobalResourceCount2{ + resourceType: resourceType, + count: total, } - return CodeBuildProjects.Projects, 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 + + m.mu.Unlock() } diff --git a/aws/lambda.go b/aws/lambda.go index 79a68c1..e3e590e 100644 --- a/aws/lambda.go +++ b/aws/lambda.go @@ -7,6 +7,7 @@ import ( "path/filepath" "sort" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" @@ -25,9 +26,11 @@ type LambdasModule struct { LambdaClient *lambda.Client IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + Goroutines int AWSProfile string SkipAdminCheck bool @@ -49,15 +52,16 @@ type Lambda struct { Region string Type string Name string + Arn string Role string Admin string CanPrivEsc string Public string } -func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string, verbosity int) { +func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) { - // These stuct values are used by the output module + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "lambda" @@ -123,24 +127,52 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string <-receiverDone // add - if struct is not empty do this. otherwise, dont write anything. - if m.pmapperError == nil { - m.output.Headers = []string{ - "Service", + m.output.Headers = []string{ + "Account", + "Region", + "Name", + "Arn", + "Role", + "IsAdminRole?", + "CanPrivEscToAdmin?", + } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", "Region", - "Resource", + "Arn", "Role", "IsAdminRole?", "CanPrivEscToAdmin?", } + // Otherwise, use the default columns. } else { - m.output.Headers = []string{ + tableCols = []string{ "Service", "Region", - "Resource", + "Name", "Role", "IsAdminRole?", + "CanPrivEscToAdmin?", } } + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") + } sort.Slice(m.Lambdas, func(i, j int) bool { return m.Lambdas[i].AWSService < m.Lambdas[j].AWSService @@ -149,41 +181,24 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string // Table rows for i := range m.Lambdas { - if m.pmapperError == nil { - m.output.Body = append( - m.output.Body, - []string{ - m.Lambdas[i].AWSService, - m.Lambdas[i].Region, - //m.Lambdas[i].Type, - m.Lambdas[i].Name, - m.Lambdas[i].Role, - m.Lambdas[i].Admin, - m.Lambdas[i].CanPrivEsc, - //m.Lambdas[i].Public, - }, - ) - } else { - m.output.Body = append( - m.output.Body, - []string{ - m.Lambdas[i].AWSService, - m.Lambdas[i].Region, - //m.Lambdas[i].Type, - m.Lambdas[i].Name, - m.Lambdas[i].Role, - m.Lambdas[i].Admin, - //m.Lambdas[i].CanPrivEsc, - //m.Lambdas[i].Public, - }, - ) - } + m.output.Body = append( + m.output.Body, + []string{ + aws.ToString(m.Caller.Account), + m.Lambdas[i].Region, + //m.Lambdas[i].Type, + m.Lambdas[i].Name, + m.Lambdas[i].Arn, + m.Lambdas[i].Role, + m.Lambdas[i].Admin, + m.Lambdas[i].CanPrivEsc, + //m.Lambdas[i].Public, + }, + ) } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -192,9 +207,10 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -300,13 +316,14 @@ func (m *LambdasModule) getLambdasPerRegion(r string, wg *sync.WaitGroup, semaph } for _, function := range functions { - //arn := aws.ToString(function.FunctionArn) + arn := aws.ToString(function.FunctionArn) name := aws.ToString(function.FunctionName) role := aws.ToString(function.Role) dataReceiver <- Lambda{ AWSService: "Lambda", Name: name, + Arn: arn, Region: r, Type: "", Role: role, diff --git a/aws/network-ports.go b/aws/network-ports.go index 142be1f..a046ed3 100644 --- a/aws/network-ports.go +++ b/aws/network-ports.go @@ -65,13 +65,15 @@ type NetworkPortsModule struct { LightsailClient *lightsail.Client RDSClient *rds.Client - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool - Verbosity int + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool + Verbosity int // Main module data IPv4_Private []NetworkService @@ -136,8 +138,8 @@ var naclToSG = map[string]string{ var validSecurityGroupProtocols = []string{"-1", "tcp", "udp"} -func (m *NetworkPortsModule) PrintNetworkPorts(outputFormat string, outputDirectory string) { - // These stuct values are used by the output module +func (m *NetworkPortsModule) PrintNetworkPorts(outputDirectory string) { + // These struct values are used by the output module m.output.Verbosity = m.Verbosity m.output.Directory = outputDirectory m.output.CallingModule = "network-ports" @@ -184,6 +186,7 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputFormat string, outputDirect // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ + "Account", "Service", "Region", "Protocol", @@ -191,6 +194,36 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputFormat string, outputDirect "Ports", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Service", + "Region", + "Protocol", + "Host", + "Ports", + } + } else { + tableCols = []string{ + "Service", + "Region", + "Protocol", + "Host", + "Ports", + } + } + // Table rows for _, arr := range [][]NetworkService{m.IPv4_Private, m.IPv4_Public, m.IPv6} { for _, i := range arr { @@ -198,6 +231,7 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputFormat string, outputDirect m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), i.AWSService, i.Region, i.Protocol, @@ -211,9 +245,6 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputFormat string, outputDirect if len(m.IPv4_Private) > 0 || len(m.IPv4_Public) > 0 || len(m.IPv6) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //internal.OutputSelector(m.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath) o := internal.OutputClient{ Verbosity: m.Verbosity, CallingModule: m.output.CallingModule, @@ -222,9 +253,10 @@ func (m *NetworkPortsModule) PrintNetworkPorts(outputFormat string, outputDirect }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -742,7 +774,7 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR // Memcached if cluster.ConfigurationEndpoint != nil { ipv4_private = addHost(ipv4_private, aws.ToString(cluster.ConfigurationEndpoint.Address)) - tcpPortsInts = addPort(tcpPortsInts, cluster.ConfigurationEndpoint.Port) + tcpPortsInts = addPort(tcpPortsInts, aws.ToInt32(cluster.ConfigurationEndpoint.Port)) } else { replicationGroupId := aws.ToString(cluster.ReplicationGroupId) for _, group := range nodes { @@ -751,7 +783,7 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR // Primary if g.PrimaryEndpoint != nil && !strContains(reportedClusters, aws.ToString(g.PrimaryEndpoint.Address)) { ipv4_private = addHost(ipv4_private, aws.ToString(g.PrimaryEndpoint.Address)) - tcpPortsInts = addPort(tcpPortsInts, g.PrimaryEndpoint.Port) + tcpPortsInts = addPort(tcpPortsInts, aws.ToInt32(g.PrimaryEndpoint.Port)) reportedClusters = addHost(reportedClusters, aws.ToString(g.PrimaryEndpoint.Address)) } @@ -759,7 +791,7 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR // Reader if g.ReaderEndpoint != nil && !strContains(reportedClusters, aws.ToString(g.ReaderEndpoint.Address)) { ipv4_private = addHost(ipv4_private, aws.ToString(g.ReaderEndpoint.Address)) - tcpPortsInts = addPort(tcpPortsInts, g.ReaderEndpoint.Port) + tcpPortsInts = addPort(tcpPortsInts, aws.ToInt32(g.ReaderEndpoint.Port)) reportedClusters = addHost(reportedClusters, aws.ToString(g.ReaderEndpoint.Address)) } @@ -769,7 +801,7 @@ func (m *NetworkPortsModule) getElastiCacheNetworkPortsPerRegion(r string, dataR if aws.ToString(m.CacheClusterId) == aws.ToString(cluster.CacheClusterId) { if m.ReadEndpoint != nil { ipv4_private = addHost(ipv4_private, aws.ToString(m.ReadEndpoint.Address)) - tcpPortsInts = addPort(tcpPortsInts, m.ReadEndpoint.Port) + tcpPortsInts = addPort(tcpPortsInts, aws.ToInt32(m.ReadEndpoint.Port)) } } } @@ -1047,7 +1079,7 @@ func (m *NetworkPortsModule) getRdsNetworkPortsPerRegion(r string, dataReceiver for _, instance := range DBInstances { if aws.ToString(instance.DBInstanceStatus) == "available" { host := []string{aws.ToString(instance.Endpoint.Address)} - var port int32 = instance.Endpoint.Port + var port int32 = aws.ToInt32(instance.Endpoint.Port) var groups []SecurityGroup for _, group := range instance.VpcSecurityGroups { diff --git a/aws/org.go b/aws/org.go index 26b9767..626116b 100644 --- a/aws/org.go +++ b/aws/org.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/aws/sdk" @@ -19,12 +20,14 @@ type OrgModule struct { OrganizationsClient sdk.OrganizationsClientInterface Caller sts.GetCallerIdentityOutput AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - SkipAdminCheck bool - WrapTable bool - DescribeOrgOutput *types.Organization + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + SkipAdminCheck bool + WrapTable bool + DescribeOrgOutput *types.Organization // Main module data Accounts []Account @@ -52,8 +55,8 @@ type Account struct { OrgId string } -func (m *OrgModule) PrintOrgAccounts(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) { + // These struct values are used by the output module var err error m.output.Verbosity = verbosity m.output.Directory = outputDirectory @@ -85,6 +88,7 @@ func (m *OrgModule) PrintOrgAccounts(outputFormat string, outputDirectory string } m.output.Headers = []string{ + "Account", "Name", "ID", "isManagementAccount?", @@ -92,12 +96,44 @@ func (m *OrgModule) PrintOrgAccounts(outputFormat string, outputDirectory string "Email", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Name", + "ID", + "isManagementAccount?", + "Status", + "Email", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Name", + "ID", + "isManagementAccount?", + "Status", + "Email", + } + } + // Table rows for i := range m.Accounts { m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Accounts[i].Name, m.Accounts[i].Id, strconv.FormatBool(m.Accounts[i].isManagementAccount), @@ -109,8 +145,6 @@ func (m *OrgModule) PrintOrgAccounts(outputFormat string, outputDirectory string if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -119,9 +153,10 @@ func (m *OrgModule) PrintOrgAccounts(outputFormat string, outputDirectory string }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -272,7 +307,7 @@ func (m *OrgModule) addOrgAccount() { isManagementAccount: false, Name: "This account", Id: aws.ToString(m.Caller.Account), - Email: "Unkonwn", + Email: "Unknown", Arn: aws.ToString(m.Caller.Arn), Status: "ACTIVE", }) diff --git a/aws/outbound-assumed-roles.go b/aws/outbound-assumed-roles.go index da28630..a65009f 100644 --- a/aws/outbound-assumed-roles.go +++ b/aws/outbound-assumed-roles.go @@ -6,6 +6,7 @@ import ( "fmt" "path/filepath" "strconv" + "strings" "sync" "time" @@ -22,12 +23,14 @@ type OutboundAssumedRolesModule struct { // General configuration data CloudTrailClient *cloudtrail.Client - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data OutboundAssumeRoleEntries []OutboundAssumeRoleEntry @@ -114,8 +117,8 @@ type CloudTrailEvent struct { } `json:"tlsDetails"` } -func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "outbound-assumed-roles" @@ -161,7 +164,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor <-receiverDone m.output.Headers = []string{ - "Service", + "Account", "Region", "Type", //"Source Account", @@ -171,12 +174,48 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor "Log Entry Timestamp", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Region", + "Type", + //"Source Account", + "Source Principal", + //"Destination Account", + "Destination Principal", + "Log Entry Timestamp", + } + // Otherwise, use the default columns. + } else { + + tableCols = []string{ + "Region", + "Type", + //"Source Account", + "Source Principal", + //"Destination Account", + "Destination Principal", + "Log Entry Timestamp", + } + } + // Table rows for i := range m.OutboundAssumeRoleEntries { m.output.Body = append( m.output.Body, []string{ - m.OutboundAssumeRoleEntries[i].AWSService, + aws.ToString(m.Caller.Account), m.OutboundAssumeRoleEntries[i].Region, m.OutboundAssumeRoleEntries[i].Type, //m.OutboundAssumeRoleEntries[i].SourceAccount, @@ -191,9 +230,6 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -202,9 +238,10 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/permissions.go b/aws/permissions.go index 3c94b56..b1e7dcd 100644 --- a/aws/permissions.go +++ b/aws/permissions.go @@ -21,12 +21,14 @@ type IamPermissionsModule struct { // General configuration data IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data @@ -76,14 +78,15 @@ type PermissionsRow struct { Arn string PolicyType string PolicyName string + PolicyArn string Effect string Action string Resource string Condition string } -func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDirectory string, verbosity int, principal string) { - // These stuct values are used by the output module +func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbosity int, principal string) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "permissions" @@ -105,12 +108,13 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi m.parsePermissions(principal) m.output.Headers = []string{ - //"Service", + "Account", "Type", "Name", - //"Arn", + "Arn", "Policy", "Policy Name", + "Policy Arn", "Effect", "Action", "Resource", @@ -122,12 +126,13 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi m.output.Body = append( m.output.Body, []string{ - //m.Rows[i].AWSService, + aws.ToString(m.Caller.Account), m.Rows[i].Type, m.Rows[i].Name, - //m.Rows[i].Arn, + m.Rows[i].Arn, m.Rows[i].PolicyType, m.Rows[i].PolicyName, + m.Rows[i].PolicyArn, m.Rows[i].Effect, m.Rows[i].Action, m.Rows[i].Resource, @@ -139,9 +144,6 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector3(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -149,10 +151,48 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi Wrap: m.WrapTable, }, } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Type", + //"Name", + "Arn", + "Policy", + "Policy Name", + "Policy Arn", + "Effect", + "Action", + "Resource", + "Condition"} + // Otherwise, use the default columns for this module (brief) + } else { + tableCols = []string{ + "Type", + "Name", + //"Arn", + "Policy", + "Policy Name", + "Effect", + "Action", + "Resource", + "Condition", + } + } + o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + TableCols: tableCols, + Body: m.output.Body, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -402,6 +442,7 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta Type: IAMtype, PolicyType: "Managed", PolicyName: p.Name, + PolicyArn: p.Arn, Effect: effect, Action: action, Resource: resource, @@ -428,6 +469,7 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta Type: IAMtype, PolicyType: "Managed", PolicyName: p.Name, + PolicyArn: p.Arn, Effect: effect, Action: "[NotAction] " + action, Resource: resource, @@ -473,6 +515,7 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline Type: IAMtype, PolicyType: "Inline", PolicyName: aws.ToString(inlinePolicy.PolicyName), + PolicyArn: aws.ToString(inlinePolicy.PolicyName), Effect: effect, Action: action, Resource: resource, @@ -498,6 +541,7 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline Type: IAMtype, PolicyType: "Inline", PolicyName: aws.ToString(inlinePolicy.PolicyName), + PolicyArn: aws.ToString(inlinePolicy.PolicyName), Effect: effect, Action: "[NotAction] " + action, Resource: resource, diff --git a/aws/pmapper.go b/aws/pmapper.go index d271ab7..34302fd 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" @@ -17,12 +18,14 @@ import ( type PmapperModule struct { // General configuration data - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data pmapperGraph graph.Graph[string, string] @@ -136,8 +139,8 @@ func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool { return false } -func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "pmapper" @@ -163,11 +166,39 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st } m.output.Headers = []string{ + "Account", "Principal Arn", "IsAdmin?", "CanPrivEscToAdmin?", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Principal Arn", + "IsAdmin?", + "CanPrivEscToAdmin?", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Principal Arn", + "IsAdmin?", + "CanPrivEscToAdmin?", + } + } + //Table rows var isAdmin, pathToAdmin string for i := range m.Nodes { @@ -186,6 +217,7 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Nodes[i].Arn, isAdmin, pathToAdmin, @@ -196,8 +228,6 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -206,9 +236,10 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/principals.go b/aws/principals.go index 7e6f02b..a403cf9 100644 --- a/aws/principals.go +++ b/aws/principals.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "strconv" + "strings" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" @@ -16,12 +17,14 @@ type IamPrincipalsModule struct { // General configuration data IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data Users []User @@ -61,8 +64,8 @@ type Role struct { InlinePolicies []string } -func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "principals" @@ -92,13 +95,44 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire //fmt.Printf("\nAnalyzed Resources by Region\n\n") m.output.Headers = []string{ - "Service", + "Account", "Type", "Name", "Arn", + "AttachedPolicies", + "InlinePolicies", + } - // "AttachedPolicies", - // "InlinePolicies", + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Type", + "Name", + "Arn", + //"AttachedPolicies", + //"InlinePolicies", + } + + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Type", + "Name", + "Arn", + // "AttachedPolicies", + // "InlinePolicies", + } } //Table rows @@ -106,13 +140,12 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire m.output.Body = append( m.output.Body, []string{ - m.Users[i].AWSService, + aws.ToString(m.Caller.Account), m.Users[i].Type, m.Users[i].Name, m.Users[i].Arn, - - // m.Users[i].AttachedPolicies, - // m.Users[i].InlinePolicies, + strings.Join(m.Users[i].AttachedPolicies, " , "), + strings.Join(m.Users[i].InlinePolicies, " , "), }, ) @@ -122,22 +155,19 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire m.output.Body = append( m.output.Body, []string{ - m.Roles[i].AWSService, + aws.ToString(m.Caller.Account), m.Roles[i].Type, m.Roles[i].Name, m.Roles[i].Arn, - - // m.Roles[i].AttachedPolicies, - // m.Roles[i].InlinePolicies, + strings.Join(m.Roles[i].AttachedPolicies, " , "), + strings.Join(m.Roles[i].InlinePolicies, " , "), }, ) } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -146,9 +176,10 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/ram.go b/aws/ram.go index 854df7d..b16ac11 100644 --- a/aws/ram.go +++ b/aws/ram.go @@ -5,6 +5,7 @@ import ( "fmt" "path/filepath" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/internal" @@ -20,12 +21,14 @@ type RAMModule struct { // General configuration data RAMClient *ram.Client - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data Resources []Resource @@ -44,8 +47,8 @@ type Resource struct { ShareType string } -func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "ram" @@ -91,7 +94,7 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ - "Service", + "Account", "Region", "Share Name", "Type", @@ -99,12 +102,44 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos "Share Type", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Region", + "Share Name", + "Type", + "Owner", + "Share Type", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Service", + "Region", + "Share Name", + "Type", + "Owner", + "Share Type", + } + } + // Table rows for i := range m.Resources { m.output.Body = append( m.output.Body, []string{ - m.Resources[i].AWSService, + aws.ToString(m.Caller.Account), m.Resources[i].Region, m.Resources[i].Name, m.Resources[i].Type, @@ -116,9 +151,7 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -127,9 +160,10 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/resource-trusts.go b/aws/resource-trusts.go index eed15bf..67dfc19 100644 --- a/aws/resource-trusts.go +++ b/aws/resource-trusts.go @@ -20,10 +20,13 @@ import ( type ResourceTrustsModule struct { // General configuration data - Caller sts.GetCallerIdentityOutput - AWSRegions []string - Goroutines int - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + Goroutines int + WrapTable bool + AWSOutputType string + AWSTableCols string + AWSProfile string CloudFoxVersion string @@ -44,10 +47,14 @@ type Resource2 struct { ResourcePolicySummary string Public string Interesting string + TrustedPrincipals string + TrustsCrossAccount string + TrustsAllAccounts string + HasConditions string } -func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "resource-trusts" @@ -60,7 +67,7 @@ func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirecto } fmt.Printf("[%s][%s] Enumerating Resources with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) - fmt.Printf("[%s][%s] Supported Services: CodeBuild, ECR, EFS, Lambda, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) + fmt.Printf("[%s][%s] Supported Services: CodeBuild, ECR, EFS, Glue, Lambda, SecretsManager, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) wg := new(sync.WaitGroup) semaphore := make(chan struct{}, m.Goroutines) @@ -95,13 +102,42 @@ func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirecto // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ - //"Account ID", + "Account", "ARN", "Public", "Interesting", "Resource Policy Summary", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "ARN", + "Public", + "Interesting", + "Resource Policy Summary", + } + } else { + tableCols = []string{ + //"Account ID", + "ARN", + "Public", + "Interesting", + "Resource Policy Summary", + } + } + // sort the table roles by Interesting sort.Slice(m.Resources2, func(i, j int) bool { return m.Resources2[j].Interesting > m.Resources2[i].Interesting @@ -112,7 +148,7 @@ func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirecto m.output.Body = append( m.output.Body, []string{ - //m.Resources2[i].AccountID, + aws.ToString(m.Caller.Account), m.Resources2[i].ARN, m.Resources2[i].Public, m.Resources2[i].Interesting, @@ -122,8 +158,6 @@ func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirecto } if len(m.output.Body) > 0 { - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -132,9 +166,10 @@ func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirecto }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -212,6 +247,24 @@ func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semap wg.Add(1) m.getEFSfilesystemPoliciesPerRegion(r, wg, semaphore, dataReceiver) } + res, err = servicemap.IsServiceInRegion("secretsmanager", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + m.getSecretsManagerSecretsPoliciesPerRegion(r, wg, semaphore, dataReceiver) + } + res, err = servicemap.IsServiceInRegion("glue", r) + if err != nil { + m.modLog.Error(err) + } + if res { + m.CommandCounter.Total++ + wg.Add(1) + m.getGlueResourcePoliciesPerRegion(r, wg, semaphore, dataReceiver) + } } @@ -270,7 +323,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou topic.IsPublic = "No" } - // If there is a resouce policy, convert the resource policy into plain english + // If there is a resource policy, convert the resource policy into plain english if !topic.Policy.IsEmpty() { for i, statement := range topic.Policy.Statement { @@ -325,7 +378,7 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s for _, b := range ListBuckets { var statementSummaryInEnglish string var isInteresting string = "No" - bucket := &Bucket{ + bucket := &BucketRow{ Arn: fmt.Sprintf("arn:aws:s3:::%s", aws.ToString(b.Name)), } name := aws.ToString(b.Name) @@ -357,7 +410,7 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s bucket.IsPublic = "No" } - // If there is a resouce policy, convert the resource policy into plain english + // If there is a resource policy, convert the resource policy into plain english if !bucket.Policy.IsEmpty() { for i, statement := range bucket.Policy.Statement { var prefix string = "" @@ -461,7 +514,7 @@ func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGro cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) - DescribeRepositories, err := cloudFoxECRClient.describeRepositories(r) + DescribeRepositories, err := sdk.CachedECRDescribeRepositories(cloudFoxECRClient.ECRClient, aws.ToString(m.Caller.Account), r) if err != nil { m.modLog.Error(err.Error()) return @@ -718,6 +771,134 @@ func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *s } } +// getSecretsManagerSecretsPoliciesPerRegion retrieves the resource policies for all Secrets Manager secrets in the specified region. +// It sends the resulting Resource2 objects to the dataReceiver channel. +// It uses a semaphore to limit the number of concurrent requests and a WaitGroup to wait for all requests to complete. +// It takes the region to search in, the WaitGroup to use, the semaphore to use, and the dataReceiver channel to send results to. +func (m *ResourceTrustsModule) getSecretsManagerSecretsPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) { + defer func() { + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + wg.Done() + + }() + semaphore <- struct{}{} + defer func() { <-semaphore }() + + cloudFoxSecretsManagerClient := InitSecretsManagerClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) + + ListSecrets, err := sdk.CachedSecretsManagerListSecrets(cloudFoxSecretsManagerClient, aws.ToString(m.Caller.Account), r) + if err != nil { + sharedLogger.Error(err.Error()) + return + } + + for _, s := range ListSecrets { + var isPublic string + var statementSummaryInEnglish string + var isInteresting string = "No" + secretPolicy, err := sdk.CachedSecretsManagerGetResourcePolicy(cloudFoxSecretsManagerClient, aws.ToString(s.ARN), r, aws.ToString(m.Caller.Account)) + if err != nil { + sharedLogger.Error(err.Error()) + m.CommandCounter.Error++ + continue + } + + if secretPolicy.IsPublic() { + isPublic = magenta("Yes") + isInteresting = magenta("Yes") + + } else { + isPublic = "No" + } + + if !secretPolicy.IsEmpty() { + for i, statement := range secretPolicy.Statement { + prefix := "" + if len(secretPolicy.Statement) > 1 { + prefix = fmt.Sprintf("Statement %d says: ", i) + statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n" + } else { + statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account) + } + statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n") + if isResourcePolicyInteresting(statementSummaryInEnglish) { + //magenta(statementSummaryInEnglish) + isInteresting = magenta("Yes") + } + + dataReceiver <- Resource2{ + AccountID: aws.ToString(m.Caller.Account), + ARN: aws.ToString(s.ARN), + ResourcePolicySummary: statementSummaryInEnglish, + Public: isPublic, + Name: aws.ToString(s.Name), + Region: r, + Interesting: isInteresting, + } + } + } + } +} + +func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) { + defer func() { + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + wg.Done() + + }() + semaphore <- struct{}{} + defer func() { <-semaphore }() + + cloudFoxGlueClient := InitGlueClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) + + ResourcePolicies, err := sdk.CachedGlueGetResourcePolicies(cloudFoxGlueClient, aws.ToString(m.Caller.Account), r) + if err != nil { + sharedLogger.Error(err.Error()) + return + } + + for _, resourcePolicy := range ResourcePolicies { + var isPublic string + var statementSummaryInEnglish string + var isInteresting string = "No" + + if resourcePolicy.IsPublic() { + isPublic = magenta("Yes") + isInteresting = magenta("Yes") + + } else { + isPublic = "No" + } + + if !resourcePolicy.IsEmpty() { + for _, statement := range resourcePolicy.Statement { + statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account) + resources := statement.Resource + for _, resource := range resources { + + statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n") + if isResourcePolicyInteresting(statementSummaryInEnglish) { + //magenta(statementSummaryInEnglish) + isInteresting = magenta("Yes") + } + + dataReceiver <- Resource2{ + AccountID: aws.ToString(m.Caller.Account), + ARN: resource, + ResourcePolicySummary: statementSummaryInEnglish, + Public: isPublic, + Name: resource, + Region: r, + Interesting: isInteresting, + } + } + } + } + } +} + func isResourcePolicyInteresting(statementSummaryInEnglish string) bool { // check if the statement has any of the following items, but make sure the check is case insensitive // if it does, then return true diff --git a/aws/resource-trusts_test.go b/aws/resource-trusts_test.go new file mode 100644 index 0000000..4a0db90 --- /dev/null +++ b/aws/resource-trusts_test.go @@ -0,0 +1,43 @@ +package aws + +import ( + "testing" +) + +func TestIsResourcePolicyInteresting(t *testing.T) { + testCases := []struct { + name string + input string + expected bool + }{ + { + name: "test1", + input: "Everyone can sqs:SendMessage & can sqs:ReceiveMessage", + expected: true, + }, + { + name: "PrincipalOrgPaths", + input: "aws:PrincipalOrgPaths", + expected: true, + }, + { + name: "Empty", + input: "", + expected: false, + }, + { + name: "NotInteresting", + input: "sns.amazonaws.com can lambda:InvokeFunction", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := isResourcePolicyInteresting(tc.input) + if actual != tc.expected { + t.Errorf("Expected %v but got %v for input %s", tc.expected, actual, tc.input) + } + }) + } +} diff --git a/aws/role-trusts.go b/aws/role-trusts.go index 96b5f3b..73dafad 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -31,12 +31,16 @@ type RoleTrustsModule struct { CommandCounter internal.CommandCounter SkipAdminCheck bool WrapTable bool - pmapperMod PmapperModule - pmapperError error - iamSimClient IamSimulatorModule + AWSOutputType string + AWSTableCols string + + pmapperMod PmapperModule + pmapperError error + iamSimClient IamSimulatorModule // Main module data - AnalyzedRoles []AnalyzedRole + AnalyzedRoles []AnalyzedRole + RoleTrustTable []RoleTrustRow // Used to store output data for pretty printing output internal.OutputData2 @@ -44,6 +48,18 @@ type RoleTrustsModule struct { modLog *logrus.Entry } +type RoleTrustRow struct { + RoleARN string + RoleName string + TrustedPrincipal string + TrustedService string + TrustedFederatedProvider string + TrustedFederatedSubject string + ExternalID string + IsAdmin string + CanPrivEsc string +} + type AnalyzedRole struct { roleARN *string trustsDoc trustPolicyDocument @@ -86,7 +102,7 @@ type RoleTrustStatementEntry struct { } `json:"Condition"` } -func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory string, verbosity int) { +func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int) { m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "role-trusts" @@ -129,25 +145,28 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - principalsHeader, principalsBody := m.printPrincipalTrusts(outputFormat, outputDirectory) + principalsHeader, principalsBody, principalTableCols := m.printPrincipalTrusts(outputDirectory) o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: principalsHeader, - Body: principalsBody, - Name: "role-trusts-principals", + Header: principalsHeader, + Body: principalsBody, + TableCols: principalTableCols, + Name: "role-trusts-principals", }) - servicesHeader, servicesBody := m.printServiceTrusts(outputFormat, outputDirectory) + servicesHeader, servicesBody, serviceTableCols := m.printServiceTrusts(outputDirectory) o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: servicesHeader, - Body: servicesBody, - Name: "role-trusts-services", + Header: servicesHeader, + Body: servicesBody, + TableCols: serviceTableCols, + Name: "role-trusts-services", }) - federatedHeader, federatedBody := m.printFederatedTrusts(outputFormat, outputDirectory) + federatedHeader, federatedBody, federatedTableCols := m.printFederatedTrusts(outputDirectory) o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: federatedHeader, - Body: federatedBody, - Name: "role-trusts-federated", + Header: federatedHeader, + Body: federatedBody, + TableCols: federatedTableCols, + Name: "role-trusts-federated", }) o.WriteFullOutput(o.Table.TableFiles, nil) @@ -169,112 +188,126 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory } -func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) { +func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]string, [][]string, []string) { var header []string var body [][]string m.output.FullFilename = "" m.output.Body = nil m.output.CallingModule = "role-trusts" m.output.FullFilename = "role-trusts-principals" - var column1 string - if m.pmapperError == nil { - header = []string{ - "Role", - "Trusted Principal", - "ExternalID", - "IsAdmin?", - "CanPrivEscToAdmin?", - } - } else { - header = []string{ - "Role", - "Trusted Principal", - "ExternalID", - "IsAdmin?", - //"CanPrivEscToAdmin?", - } + header = []string{ + "Account", + "Role Arn", + "Role Name", + "Trusted Principal", + "ExternalID", + "IsAdmin?", + "CanPrivEscToAdmin?", + } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{"Account", "Role Arn", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"} + // Otherwise, use the default columns for this module (brief) + } else { + tableCols = []string{"Role Name", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"} + } + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") } for _, role := range m.AnalyzedRoles { for _, statement := range role.trustsDoc.Statement { for _, principal := range statement.Principal.AWS { - if outputFormat == "wide" { - column1 = aws.ToString(role.roleARN) - } else { - column1 = GetResourceNameFromArn(aws.ToString(role.roleARN)) - } - column2 := principal - column3 := statement.Condition.StringEquals.StsExternalID - column4 := role.Admin - column5 := role.CanPrivEsc - if m.pmapperError == nil { - body = append(body, []string{column1, column2, column3, column4, column5}) - } else { - body = append(body, []string{column1, column2, column3, column4}) - } + RoleTrustRow := RoleTrustRow{ + RoleARN: aws.ToString(role.roleARN), + RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)), + TrustedPrincipal: principal, + ExternalID: statement.Condition.StringEquals.StsExternalID, + IsAdmin: role.Admin, + CanPrivEsc: role.CanPrivEsc, + } + body = append(body, []string{ + aws.ToString(m.Caller.Account), + RoleTrustRow.RoleARN, + RoleTrustRow.RoleName, + RoleTrustRow.TrustedPrincipal, + RoleTrustRow.ExternalID, + RoleTrustRow.IsAdmin, + RoleTrustRow.CanPrivEsc}) } } } + m.sortTrustsTablePerTrustedPrincipal() - return header, body - // if len(m.output.Body) > 0 { - // m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile)) - // ////m.output.OutputSelector(outputFormat) - // //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule) - // //internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile) - // fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body))) + return header, body, tableCols - // } else { - // fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - // } - // fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule) } -func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) { +func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string, [][]string, []string) { var header []string var body [][]string m.output.FullFilename = "" m.output.Body = nil m.output.CallingModule = "role-trusts" m.output.FullFilename = "role-trusts-services" - var column1 string - if m.pmapperError == nil { - header = []string{ - "Role", - "Trusted Service", - "ExternalID", - "IsAdmin?", - "CanPrivEscToAdmin?", - } + header = []string{ + "Account", + "Role Arn", + "Role Name", + "Trusted Service", + "IsAdmin?", + "CanPrivEscToAdmin?", + } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{"Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"} + // Otherwise, use the default columns for this module (brief) } else { - header = []string{ - "Role", - "Trusted Service", - "ExternalID", - "IsAdmin?", - //"CanPrivEscToAdmin?", - } + tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"} + } + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") } for _, role := range m.AnalyzedRoles { for _, statement := range role.trustsDoc.Statement { for _, service := range statement.Principal.Service { - if outputFormat == "wide" { - column1 = aws.ToString(role.roleARN) - } else { - column1 = GetResourceNameFromArn(aws.ToString(role.roleARN)) - } - column2 := service - column3 := statement.Condition.StringEquals.StsExternalID - column4 := role.Admin - column5 := role.CanPrivEsc - if m.pmapperError == nil { - body = append(body, []string{column1, column2, column3, column4, column5}) - } else { - body = append(body, []string{column1, column2, column3, column4}) + RoleTrustRow := RoleTrustRow{ + RoleARN: aws.ToString(role.roleARN), + RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)), + TrustedService: service, + IsAdmin: role.Admin, + CanPrivEsc: role.CanPrivEsc, } + body = append(body, []string{ + aws.ToString(m.Caller.Account), + RoleTrustRow.RoleARN, + RoleTrustRow.RoleName, + RoleTrustRow.TrustedService, + RoleTrustRow.IsAdmin, + RoleTrustRow.CanPrivEsc}) + } } } @@ -284,81 +317,74 @@ func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirecto return body[i][1] < body[j][1] }) - //m.sortTrustsTablePerTrustedPrincipal() - return header, body - - // if len(m.output.Body) > 0 { - // m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile)) - // //m.output.OutputSelector(outputFormat) - // //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule) - // internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile) - // fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body))) + return header, body, tableCols - // } else { - // fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - // } } -func (m *RoleTrustsModule) printFederatedTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) { +func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]string, [][]string, []string) { var header []string var body [][]string m.output.FullFilename = "" m.output.Body = nil m.output.CallingModule = "role-trusts" m.output.FullFilename = "role-trusts-federated" - var column1, column3, column2 string - if m.pmapperError == nil { + header = []string{ + "Account", + "Role Arn", + "Role Name", + "Trusted Provider", + "Trusted Subject", + "IsAdmin?", + "CanPrivEscToAdmin?", + } - header = []string{ - "Role", - "Trusted Provider", - "Trusted Subject", - "IsAdmin?", - "CanPrivEscToAdmin?", - } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{"Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"} + // Otherwise, use the default columns for this module (brief) } else { - header = []string{ - "Role", - "Trusted Provider", - "Trusted Subject", - "IsAdmin?", - //"CanPrivEscToAdmin?", - } + tableCols = []string{"Role Name", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"} + } + // Remove the pmapper row if there is no pmapper data + if m.pmapperError != nil { + sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile) + tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?") } - for _, role := range m.AnalyzedRoles { - if outputFormat == "wide" { - column1 = aws.ToString(role.roleARN) - } else { - column1 = GetResourceNameFromArn(aws.ToString(role.roleARN)) - } for _, statement := range role.trustsDoc.Statement { if len(statement.Principal.Federated) > 0 { - column2, column3 = parseFederatedTrustPolicy(statement) - column4 := role.Admin - column5 := role.CanPrivEsc - if m.pmapperError == nil { - body = append(body, []string{column1, column2, column3, column4, column5}) - } else { - body = append(body, []string{column1, column2, column3, column4}) + provider, subject := parseFederatedTrustPolicy(statement) + RoleTrustRow := RoleTrustRow{ + RoleARN: aws.ToString(role.roleARN), + RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)), + TrustedFederatedProvider: provider, + TrustedFederatedSubject: subject, + IsAdmin: role.Admin, + CanPrivEsc: role.CanPrivEsc, } + body = append(body, []string{ + aws.ToString(m.Caller.Account), + RoleTrustRow.RoleARN, + RoleTrustRow.RoleName, + RoleTrustRow.TrustedFederatedProvider, + RoleTrustRow.TrustedFederatedSubject, + RoleTrustRow.IsAdmin, + RoleTrustRow.CanPrivEsc}) } + } } m.sortTrustsTablePerTrustedPrincipal() - return header, body - - // if len(m.output.Body) > 0 { - // m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile)) - // //m.output.OutputSelector(outputFormat) - // //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule) - // internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile) - // fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body))) + return header, body, tableCols - // } else { - // fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - // } } func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) { @@ -435,6 +461,7 @@ func (m *RoleTrustsModule) getAllRoleTrusts() { Admin: "", CanPrivEsc: "", }) + } } diff --git a/aws/route53.go b/aws/route53.go index 91b0c67..1a7f2ac 100644 --- a/aws/route53.go +++ b/aws/route53.go @@ -1,26 +1,28 @@ package aws import ( - "context" "fmt" "os" "path/filepath" "strconv" + "strings" + "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/sirupsen/logrus" ) type Route53Module struct { // General configuration data - Route53Client *route53.Client + Route53Client sdk.AWSRoute53ClientInterface + + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string Goroutines int AWSProfile string WrapTable bool @@ -42,9 +44,9 @@ type Record struct { PrivateZone string } -func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string, verbosity int) { +func (m *Route53Module) PrintRoute53(outputDirectory string, verbosity int) { - // These stuct values are used by the output module + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "route53" @@ -60,19 +62,48 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string m.getRoute53Records() m.output.Headers = []string{ - "Service", + "Account", "Name", "Type", "Value", "PrivateZone", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Name", + "Type", + "Value", + "PrivateZone", + } + + } else { + tableCols = []string{ + "Name", + "Type", + "Value", + "PrivateZone", + } + } + // Table rows for i := range m.Records { m.output.Body = append( m.output.Body, []string{ - m.Records[i].AWSService, + aws.ToString(m.Caller.Account), m.Records[i].Name, m.Records[i].Type, m.Records[i].Value, @@ -84,10 +115,7 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -96,9 +124,10 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -127,7 +156,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) { var route53APublicRecords string for _, record := range m.Records { - if record.Type == "A" || record.Type == "AAAA" { + if record.Type == "A" || record.Type == "AAAA" || record.Type == "CNAME" { if record.PrivateZone == "True" { route53APrivateRecords = route53APrivateRecords + fmt.Sprintln(record.Name) } else { @@ -176,71 +205,51 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) { func (m *Route53Module) getRoute53Records() { // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. - var PaginationControl *string var recordName string var recordType string - for { - ListHostedZones, err := m.Route53Client.ListHostedZones( - context.TODO(), - &route53.ListHostedZonesInput{ - Marker: PaginationControl, - }, - ) + HostedZones, err := sdk.CachedRoute53ListHostedZones(m.Route53Client, aws.ToString(m.Caller.Account)) + + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + return + } + + var privateZone string + for _, zone := range HostedZones { + if zone.Config.PrivateZone { + privateZone = "True" + } else { + privateZone = "False" + } + + Records, err := sdk.CachedRoute53ListResourceRecordSets(m.Route53Client, aws.ToString(m.Caller.Account), aws.ToString(zone.Id)) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ break } + for _, record := range Records { + recordName = aws.ToString(record.Name) + recordType = string(record.Type) + + for _, resourceRecord := range record.ResourceRecords { + recordValue := resourceRecord.Value + m.Records = append( + m.Records, + Record{ + AWSService: "Route53", + Name: recordName, + Type: recordType, + Value: aws.ToString(recordValue), + PrivateZone: privateZone, + }) - var privateZone string - for _, zone := range ListHostedZones.HostedZones { - id := aws.ToString(zone.Id) - if zone.Config.PrivateZone { - privateZone = "True" - } else { - privateZone = "False" - } - - ListResourceRecordSets, err := m.Route53Client.ListResourceRecordSets( - context.TODO(), - &route53.ListResourceRecordSetsInput{ - HostedZoneId: &id, - }, - ) - if err != nil { - m.modLog.Error(err.Error()) - m.CommandCounter.Error++ - break - } - for _, record := range ListResourceRecordSets.ResourceRecordSets { - recordName = aws.ToString(record.Name) - recordType = string(record.Type) - - for _, resourceRecord := range record.ResourceRecords { - recordValue := resourceRecord.Value - m.Records = append( - m.Records, - Record{ - AWSService: "Route53", - Name: recordName, - Type: recordType, - Value: aws.ToString(recordValue), - PrivateZone: privateZone, - }) - - } } - } - // The "NextToken" value is nil when there's no more data to return. - if ListHostedZones.NextMarker != nil { - PaginationControl = ListHostedZones.NextMarker - } else { - PaginationControl = nil - break - } } + } diff --git a/aws/sdk/apigateway.go b/aws/sdk/apigateway.go index e5cb703..018b23e 100644 --- a/aws/sdk/apigateway.go +++ b/aws/sdk/apigateway.go @@ -14,7 +14,7 @@ type APIGatewayClientInterface interface { GetRestApis(context.Context, *apigateway.GetRestApisInput, ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error) } -func RegisterApiGatewayTypes() { +func init() { gob.Register([]apiGatewayTypes.RestApi{}) } diff --git a/aws/sdk/apigateway_mocks.go b/aws/sdk/apigateway_mocks.go new file mode 100644 index 0000000..88b7676 --- /dev/null +++ b/aws/sdk/apigateway_mocks.go @@ -0,0 +1,27 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types" +) + +type MockedAWSAPIGatewayClient struct { +} + +func (m *MockedAWSAPIGatewayClient) GetRestApis(ctx context.Context, input *apigateway.GetRestApisInput, options ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error) { + return &apigateway.GetRestApisOutput{ + Items: []apiGatewayTypes.RestApi{ + { + Id: aws.String("api1"), + Name: aws.String("api1"), + }, + { + Id: aws.String("api2"), + Name: aws.String("api2"), + }, + }, + }, nil +} diff --git a/aws/sdk/apigatewayv2.go b/aws/sdk/apigatewayv2.go index e26cc66..b4aed5e 100644 --- a/aws/sdk/apigatewayv2.go +++ b/aws/sdk/apigatewayv2.go @@ -16,7 +16,7 @@ type APIGatewayv2ClientInterface interface { GetApis(context.Context, *apigatewayv2.GetApisInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error) } -func RegisterApiGatewayV2Types() { +func init() { gob.Register([]apiGatwayV2Types.Api{}) } diff --git a/aws/sdk/apigatewayv2_mocks.go b/aws/sdk/apigatewayv2_mocks.go new file mode 100644 index 0000000..3908b07 --- /dev/null +++ b/aws/sdk/apigatewayv2_mocks.go @@ -0,0 +1,27 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigatewayv2" + apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types" +) + +type MockedAWSAPIGatewayv2Client struct { +} + +func (m *MockedAWSAPIGatewayv2Client) GetApis(ctx context.Context, input *apigatewayv2.GetApisInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error) { + return &apigatewayv2.GetApisOutput{ + Items: []apiGatwayV2Types.Api{ + { + ApiId: aws.String("api1"), + Name: aws.String("api1"), + }, + { + ApiId: aws.String("api2"), + Name: aws.String("api2"), + }, + }, + }, nil +} diff --git a/aws/sdk/apprunner.go b/aws/sdk/apprunner.go index 0d15dba..b46d236 100644 --- a/aws/sdk/apprunner.go +++ b/aws/sdk/apprunner.go @@ -15,7 +15,7 @@ type AppRunnerClientInterface interface { ListServices(context.Context, *apprunner.ListServicesInput, ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error) } -func RegisterApprunnerTypes() { +func init() { gob.Register([]apprunnerTypes.Service{}) gob.Register([]apprunnerTypes.ServiceSummary{}) } diff --git a/aws/sdk/apprunner_mocks.go b/aws/sdk/apprunner_mocks.go new file mode 100644 index 0000000..bd9e847 --- /dev/null +++ b/aws/sdk/apprunner_mocks.go @@ -0,0 +1,25 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apprunner" + apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types" +) + +type MockedAppRunnerClient struct { +} + +func (m *MockedAppRunnerClient) ListServices(ctx context.Context, input *apprunner.ListServicesInput, options ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error) { + return &apprunner.ListServicesOutput{ + ServiceSummaryList: []apprunnerTypes.ServiceSummary{ + { + ServiceName: aws.String("service1"), + }, + { + ServiceName: aws.String("service2"), + }, + }, + }, nil +} diff --git a/aws/sdk/athena.go b/aws/sdk/athena.go new file mode 100644 index 0000000..d7085b3 --- /dev/null +++ b/aws/sdk/athena.go @@ -0,0 +1,96 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/athena" + athenaTypes "github.com/aws/aws-sdk-go-v2/service/athena/types" + "github.com/patrickmn/go-cache" +) + +type AWSAthenaClientInterface interface { + ListDataCatalogs(context.Context, *athena.ListDataCatalogsInput, ...func(*athena.Options)) (*athena.ListDataCatalogsOutput, error) + ListDatabases(context.Context, *athena.ListDatabasesInput, ...func(*athena.Options)) (*athena.ListDatabasesOutput, error) +} + +func init() { + gob.Register([]athenaTypes.DataCatalogSummary{}) +} + +func CachedAthenaListDataCatalogs(client AWSAthenaClientInterface, accountID string, region string) ([]athenaTypes.DataCatalogSummary, error) { + var PaginationControl *string + var dataCatalogs []athenaTypes.DataCatalogSummary + cacheKey := fmt.Sprintf("%s-athena-ListDataCatalogs-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]athenaTypes.DataCatalogSummary), nil + } + for { + ListDataCatalogs, err := client.ListDataCatalogs( + context.TODO(), + &athena.ListDataCatalogsInput{ + NextToken: PaginationControl, + }, + func(o *athena.Options) { + o.Region = region + }, + ) + + if err != nil { + return dataCatalogs, err + } + + dataCatalogs = append(dataCatalogs, ListDataCatalogs.DataCatalogsSummary...) + + //pagination + if ListDataCatalogs.NextToken == nil { + break + } + PaginationControl = ListDataCatalogs.NextToken + } + + internal.Cache.Set(cacheKey, dataCatalogs, cache.DefaultExpiration) + return dataCatalogs, nil +} + +func CachedAthenaListDatabases(client AWSAthenaClientInterface, accountID string, region string, catalogName string) ([]string, error) { + var PaginationControl *string + var databases []string + cacheKey := fmt.Sprintf("%s-athena-ListDatabases-%s-%s", accountID, region, catalogName) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]string), nil + } + for { + ListDatabases, err := client.ListDatabases( + context.TODO(), + &athena.ListDatabasesInput{ + CatalogName: &catalogName, + NextToken: PaginationControl, + }, + func(o *athena.Options) { + o.Region = region + }, + ) + + if err != nil { + return databases, err + } + + for _, database := range ListDatabases.DatabaseList { + databases = append(databases, *database.Name) + } + + //pagination + if ListDatabases.NextToken == nil { + break + } + PaginationControl = ListDatabases.NextToken + } + + internal.Cache.Set(cacheKey, databases, cache.DefaultExpiration) + return databases, nil +} diff --git a/aws/sdk/athena_mocks.go b/aws/sdk/athena_mocks.go new file mode 100644 index 0000000..608ce04 --- /dev/null +++ b/aws/sdk/athena_mocks.go @@ -0,0 +1,38 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/athena" + athenaTypes "github.com/aws/aws-sdk-go-v2/service/athena/types" +) + +type MockedAWSAthenaClient struct { +} + +func (m *MockedAWSAthenaClient) ListDatabases(ctx context.Context, input *athena.ListDatabasesInput, options ...func(*athena.Options)) (*athena.ListDatabasesOutput, error) { + return &athena.ListDatabasesOutput{ + DatabaseList: []athenaTypes.Database{ + { + Name: aws.String("db1"), + }, + { + Name: aws.String("db2"), + }, + }, + }, nil +} + +func (m *MockedAWSAthenaClient) ListDataCatalogs(ctx context.Context, input *athena.ListDataCatalogsInput, options ...func(*athena.Options)) (*athena.ListDataCatalogsOutput, error) { + return &athena.ListDataCatalogsOutput{ + DataCatalogsSummary: []athenaTypes.DataCatalogSummary{ + { + CatalogName: aws.String("catalog1"), + }, + { + CatalogName: aws.String("catalog2"), + }, + }, + }, nil +} diff --git a/aws/sdk/cloud9.go b/aws/sdk/cloud9.go new file mode 100644 index 0000000..c9fd448 --- /dev/null +++ b/aws/sdk/cloud9.go @@ -0,0 +1,86 @@ +package sdk + +import ( + "context" + "encoding/gob" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/cloud9" + cloud9Types "github.com/aws/aws-sdk-go-v2/service/cloud9/types" + "github.com/patrickmn/go-cache" +) + +type AWSCloud9ClientInterface interface { + ListEnvironments(context.Context, *cloud9.ListEnvironmentsInput, ...func(*cloud9.Options)) (*cloud9.ListEnvironmentsOutput, error) + DescribeEnvironments(context.Context, *cloud9.DescribeEnvironmentsInput, ...func(*cloud9.Options)) (*cloud9.DescribeEnvironmentsOutput, error) +} + +func init() { + gob.Register([]string{}) +} + +func CachedCloud9ListEnvironments(client AWSCloud9ClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var environments []string + cacheKey := "cloud9-ListEnvironments-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]string), nil + } + for { + ListEnvironments, err := client.ListEnvironments( + context.TODO(), + &cloud9.ListEnvironmentsInput{ + NextToken: PaginationControl, + }, + func(o *cloud9.Options) { + o.Region = region + }, + ) + + if err != nil { + return environments, err + } + + environments = append(environments, ListEnvironments.EnvironmentIds...) + + //pagination + if ListEnvironments.NextToken == nil { + break + } + PaginationControl = ListEnvironments.NextToken + } + + internal.Cache.Set(cacheKey, environments, cache.DefaultExpiration) + return environments, nil +} + +func CachedCloud9DescribeEnvironments(client AWSCloud9ClientInterface, accountID string, region string, environmentIDs []string) ([]cloud9Types.Environment, error) { + var environments []cloud9Types.Environment + cacheKey := "cloud9-DescribeEnvironments-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]cloud9Types.Environment), nil + } + for _, environmentID := range environmentIDs { + DescribeEnvironments, err := client.DescribeEnvironments( + context.TODO(), + &cloud9.DescribeEnvironmentsInput{ + EnvironmentIds: []string{environmentID}, + }, + func(o *cloud9.Options) { + o.Region = region + }, + ) + + if err != nil { + return environments, err + } + + environments = append(environments, DescribeEnvironments.Environments...) + + } + + internal.Cache.Set(cacheKey, environments, cache.DefaultExpiration) + return environments, nil +} diff --git a/aws/sdk/cloud9_mocks.go b/aws/sdk/cloud9_mocks.go new file mode 100644 index 0000000..862b357 --- /dev/null +++ b/aws/sdk/cloud9_mocks.go @@ -0,0 +1,36 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloud9" + "github.com/aws/aws-sdk-go-v2/service/cloud9/types" +) + +type MockedAWSCloud9Client struct { +} + +func (m *MockedAWSCloud9Client) ListEnvironments(ctx context.Context, input *cloud9.ListEnvironmentsInput, options ...func(*cloud9.Options)) (*cloud9.ListEnvironmentsOutput, error) { + return &cloud9.ListEnvironmentsOutput{ + EnvironmentIds: []string{ + "env1", + "env2", + }, + }, nil +} + +func (m *MockedAWSCloud9Client) DescribeEnvironments(ctx context.Context, input *cloud9.DescribeEnvironmentsInput, options ...func(*cloud9.Options)) (*cloud9.DescribeEnvironmentsOutput, error) { + return &cloud9.DescribeEnvironmentsOutput{ + Environments: []types.Environment{ + { + Name: aws.String("env1"), + Arn: aws.String("arn:aws:cloud9:us-east-1:123456789012:environment/env1"), + }, + { + Name: aws.String("env2"), + Arn: aws.String("arn:aws:cloud9:us-east-1:123456789012:environment/env2"), + }, + }, + }, nil +} diff --git a/aws/sdk/cloudformation.go b/aws/sdk/cloudformation.go index 8ac5326..316e4af 100644 --- a/aws/sdk/cloudformation.go +++ b/aws/sdk/cloudformation.go @@ -18,7 +18,7 @@ type CloudFormationClientInterface interface { ListStacks(context.Context, *cloudformation.ListStacksInput, ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) } -func RegisterCloudFormationTypes() { +func init() { gob.Register([]cloudFormationTypes.Stack{}) gob.Register([]cloudFormationTypes.StackSummary{}) } diff --git a/aws/sdk/cloudformation_mocks.go b/aws/sdk/cloudformation_mocks.go new file mode 100644 index 0000000..4e924be --- /dev/null +++ b/aws/sdk/cloudformation_mocks.go @@ -0,0 +1,73 @@ +package sdk + +import ( + "context" + "encoding/json" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/service/cloudformation" + "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" +) + +const DESCRIBE_STACKS_TEST_FILE = "./test-data/cloudformation-describestacks.json" +const TEMPLATE_BODY_TEST_FILE = "./test-data/cloudformation-getTemplate.json" + +type Stacks struct { + Stacks []struct { + StackID string `json:"StackId"` + Description string `json:"Description"` + Tags []interface{} `json:"Tags"` + Outputs []types.Output `json:"Outputs"` + Parameters []types.Parameter `json:"Parameters"` + StackStatusReason interface{} `json:"StackStatusReason"` + CreationTime time.Time `json:"CreationTime"` + Capabilities []interface{} `json:"Capabilities"` + StackName string `json:"StackName"` + RoleArn string `json:"RoleArn"` + StackStatus string `json:"StackStatus"` + DisableRollback bool `json:"DisableRollback"` + } `json:"Stacks"` +} + +type TemplateBody struct { + TemplateBody string `json:"TemplateBody"` +} + +type MockedCloudformationClient struct { + describeStacks Stacks + getTemplateBody TemplateBody +} + +func (m *MockedCloudformationClient) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) { + + err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.describeStacks) + if err != nil { + log.Fatalf("can't unmarshall file %s", DESCRIBE_STACKS_TEST_FILE) + } + var stacks []types.Stack + for _, stack := range m.describeStacks.Stacks { + stacks = append(stacks, types.Stack{ + StackName: &stack.StackName, + RoleARN: &stack.RoleArn, + Outputs: stack.Outputs, + Parameters: stack.Parameters, + }) + + } + + return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil +} + +func (m *MockedCloudformationClient) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) { + err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.getTemplateBody) + if err != nil { + log.Fatalf("can't unmarshall file %s", TEMPLATE_BODY_TEST_FILE) + } + + return &cloudformation.GetTemplateOutput{TemplateBody: &m.getTemplateBody.TemplateBody}, nil +} + +func (m *MockedCloudformationClient) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) { + return &cloudformation.ListStacksOutput{}, nil +} diff --git a/aws/sdk/cloudfront.go b/aws/sdk/cloudfront.go new file mode 100644 index 0000000..ecbf2ab --- /dev/null +++ b/aws/sdk/cloudfront.go @@ -0,0 +1,57 @@ +package sdk + +import ( + "context" + "encoding/gob" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/cloudfront" + cloudfrontTypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" + "github.com/patrickmn/go-cache" +) + +type AWSCloudFrontClientInterface interface { + ListDistributions(ctx context.Context, params *cloudfront.ListDistributionsInput, optFns ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error) +} + +func init() { + gob.Register([]cloudfrontTypes.DistributionSummary{}) + gob.Register(cloudfrontTypes.DistributionSummary{}) +} + +func CachedCloudFrontListDistributions(CloudFrontClient AWSCloudFrontClientInterface, accountID string) ([]cloudfrontTypes.DistributionSummary, error) { + var PaginationControl *string + var distributions []cloudfrontTypes.DistributionSummary + cacheKey := "cloudfront-ListDistributions-" + accountID + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached CloudFront distributions data") + return cached.([]cloudfrontTypes.DistributionSummary), nil + } + + for { + ListDistributions, err := CloudFrontClient.ListDistributions( + context.TODO(), + &cloudfront.ListDistributionsInput{ + Marker: PaginationControl, + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + distributions = append(distributions, ListDistributions.DistributionList.Items...) + + // Pagination control. + if ListDistributions.DistributionList.NextMarker != nil { + PaginationControl = ListDistributions.DistributionList.NextMarker + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, distributions, cache.DefaultExpiration) + return distributions, nil +} diff --git a/aws/sdk/cloudfront_mocks.go b/aws/sdk/cloudfront_mocks.go new file mode 100644 index 0000000..8b75f31 --- /dev/null +++ b/aws/sdk/cloudfront_mocks.go @@ -0,0 +1,27 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudfront" + cloudfrontTypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" +) + +type MockedAWSCloudFrontClient struct { +} + +func (m *MockedAWSCloudFrontClient) ListDistributions(ctx context.Context, input *cloudfront.ListDistributionsInput, options ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error) { + return &cloudfront.ListDistributionsOutput{ + DistributionList: &cloudfrontTypes.DistributionList{ + Items: []cloudfrontTypes.DistributionSummary{ + { + Id: aws.String("distribution1"), + }, + { + Id: aws.String("distribution2"), + }, + }, + }, + }, nil +} diff --git a/aws/sdk/codeartifact.go b/aws/sdk/codeartifact.go new file mode 100644 index 0000000..b0950b1 --- /dev/null +++ b/aws/sdk/codeartifact.go @@ -0,0 +1,94 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/codeartifact" + codeArtifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types" + "github.com/patrickmn/go-cache" +) + +type AWSCodeArtifactClientInterface interface { + ListDomains(context.Context, *codeartifact.ListDomainsInput, ...func(*codeartifact.Options)) (*codeartifact.ListDomainsOutput, error) + ListRepositories(context.Context, *codeartifact.ListRepositoriesInput, ...func(*codeartifact.Options)) (*codeartifact.ListRepositoriesOutput, error) +} + +func init() { + gob.Register([]codeArtifactTypes.DomainSummary{}) + gob.Register([]codeArtifactTypes.RepositorySummary{}) +} + +func CachedCodeArtifactListDomains(client AWSCodeArtifactClientInterface, accountID string, region string) ([]codeArtifactTypes.DomainSummary, error) { + var PaginationControl *string + var domains []codeArtifactTypes.DomainSummary + cacheKey := fmt.Sprintf("%s-codeartifact-ListDomains-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]codeArtifactTypes.DomainSummary), nil + } + for { + ListDomains, err := client.ListDomains( + context.TODO(), + &codeartifact.ListDomainsInput{ + NextToken: PaginationControl, + }, + func(o *codeartifact.Options) { + o.Region = region + }, + ) + + if err != nil { + return domains, err + } + + domains = append(domains, ListDomains.Domains...) + + //pagination + if ListDomains.NextToken == nil { + break + } + PaginationControl = ListDomains.NextToken + } + + internal.Cache.Set(cacheKey, domains, cache.DefaultExpiration) + return domains, nil +} + +func CachedCodeArtifactListRepositories(client AWSCodeArtifactClientInterface, accountID string, region string) ([]codeArtifactTypes.RepositorySummary, error) { + var PaginationControl *string + var repositories []codeArtifactTypes.RepositorySummary + cacheKey := fmt.Sprintf("%s-codeartifact-ListRepositories-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]codeArtifactTypes.RepositorySummary), nil + } + for { + ListRepositories, err := client.ListRepositories( + context.TODO(), + &codeartifact.ListRepositoriesInput{ + NextToken: PaginationControl, + }, + func(o *codeartifact.Options) { + o.Region = region + }, + ) + + if err != nil { + return repositories, err + } + + repositories = append(repositories, ListRepositories.Repositories...) + + //pagination + if ListRepositories.NextToken == nil { + break + } + PaginationControl = ListRepositories.NextToken + } + + internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration) + return repositories, nil +} diff --git a/aws/sdk/codeartifact_mocks.go b/aws/sdk/codeartifact_mocks.go new file mode 100644 index 0000000..b9b0419 --- /dev/null +++ b/aws/sdk/codeartifact_mocks.go @@ -0,0 +1,46 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/codeartifact" + codeArtifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types" +) + +type MockedAWSCodeArtifactClient struct { +} + +func (m *MockedAWSCodeArtifactClient) ListRepositories(ctx context.Context, input *codeartifact.ListRepositoriesInput, options ...func(*codeartifact.Options)) (*codeartifact.ListRepositoriesOutput, error) { + return &codeartifact.ListRepositoriesOutput{ + NextToken: nil, + Repositories: []codeArtifactTypes.RepositorySummary{ + { + Name: aws.String("repo1"), + Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:repository/repo1"), + DomainName: aws.String("domain1"), + }, + { + Name: aws.String("repo2"), + Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:repository/repo2"), + DomainName: aws.String("domain1"), + }, + }, + }, nil +} + +func (m *MockedAWSCodeArtifactClient) ListDomains(ctx context.Context, input *codeartifact.ListDomainsInput, options ...func(*codeartifact.Options)) (*codeartifact.ListDomainsOutput, error) { + return &codeartifact.ListDomainsOutput{ + NextToken: nil, + Domains: []codeArtifactTypes.DomainSummary{ + { + Name: aws.String("domain1"), + Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:domain/domain1"), + }, + { + Name: aws.String("domain2"), + Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:domain/domain2"), + }, + }, + }, nil +} diff --git a/aws/sdk/codebuild.go b/aws/sdk/codebuild.go index f2ccb6c..ba5bd3d 100644 --- a/aws/sdk/codebuild.go +++ b/aws/sdk/codebuild.go @@ -18,7 +18,7 @@ type CodeBuildClientInterface interface { GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) } -func RegisterCodeBuildTypes() { +func init() { gob.Register(codeBuildTypes.Project{}) } diff --git a/aws/sdk/codebuild_mocks.go b/aws/sdk/codebuild_mocks.go new file mode 100644 index 0000000..efbaea2 --- /dev/null +++ b/aws/sdk/codebuild_mocks.go @@ -0,0 +1,39 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/codebuild" +) + +type MockedCodeBuildClient struct { +} + +func (m *MockedCodeBuildClient) ListProjects(ctx context.Context, input *codebuild.ListProjectsInput, options ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) { + return &codebuild.ListProjectsOutput{ + Projects: []string{ + "project1", + "project2", + }, + }, nil +} + +func (m *MockedCodeBuildClient) GetResourcePolicy(ctx context.Context, input *codebuild.GetResourcePolicyInput, options ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) { + return &codebuild.GetResourcePolicyOutput{ + Policy: aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "codebuild:BatchGetProjects", + "Resource": "*", + "Principal": { + "AWS": "arn:aws:iam::123456789012:root" + }, + } + ] + } + `), + }, nil +} diff --git a/aws/sdk/codecommit.go b/aws/sdk/codecommit.go new file mode 100644 index 0000000..32428c4 --- /dev/null +++ b/aws/sdk/codecommit.go @@ -0,0 +1,56 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/codecommit" + codeCommitTypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types" + "github.com/patrickmn/go-cache" +) + +type AWSCodeCommitClientInterface interface { + ListRepositories(context.Context, *codecommit.ListRepositoriesInput, ...func(*codecommit.Options)) (*codecommit.ListRepositoriesOutput, error) +} + +func init() { + gob.Register([]codeCommitTypes.RepositoryNameIdPair{}) +} + +func CachedCodeCommitListRepositories(client AWSCodeCommitClientInterface, accountID string, region string) ([]codeCommitTypes.RepositoryNameIdPair, error) { + var PaginationControl *string + var repositories []codeCommitTypes.RepositoryNameIdPair + cacheKey := fmt.Sprintf("%s-codecommit-ListRepositories-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]codeCommitTypes.RepositoryNameIdPair), nil + } + for { + ListRepositories, err := client.ListRepositories( + context.TODO(), + &codecommit.ListRepositoriesInput{ + NextToken: PaginationControl, + }, + func(o *codecommit.Options) { + o.Region = region + }, + ) + + if err != nil { + return repositories, err + } + + repositories = append(repositories, ListRepositories.Repositories...) + + //pagination + if ListRepositories.NextToken == nil { + break + } + PaginationControl = ListRepositories.NextToken + } + + internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration) + return repositories, nil +} diff --git a/aws/sdk/codecommit_mocks.go b/aws/sdk/codecommit_mocks.go new file mode 100644 index 0000000..ab48715 --- /dev/null +++ b/aws/sdk/codecommit_mocks.go @@ -0,0 +1,30 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/codecommit" + codeCommitTypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types" +) + +// create mocks for codecommit ListRepositories + +type MockedAWSCodeCommitClient struct { +} + +func (m *MockedAWSCodeCommitClient) ListRepositories(ctx context.Context, input *codecommit.ListRepositoriesInput, options ...func(*codecommit.Options)) (*codecommit.ListRepositoriesOutput, error) { + return &codecommit.ListRepositoriesOutput{ + NextToken: nil, + Repositories: []codeCommitTypes.RepositoryNameIdPair{ + { + RepositoryId: aws.String("repo1"), + RepositoryName: aws.String("repo1"), + }, + { + RepositoryId: aws.String("repo2"), + RepositoryName: aws.String("repo2"), + }, + }, + }, nil +} diff --git a/aws/sdk/codedeploy.go b/aws/sdk/codedeploy.go new file mode 100644 index 0000000..640d12d --- /dev/null +++ b/aws/sdk/codedeploy.go @@ -0,0 +1,212 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/codedeploy" + codeDeployTypes "github.com/aws/aws-sdk-go-v2/service/codedeploy/types" + "github.com/patrickmn/go-cache" +) + +type AWSCodeDeployClientInterface interface { + ListApplications(context.Context, *codedeploy.ListApplicationsInput, ...func(*codedeploy.Options)) (*codedeploy.ListApplicationsOutput, error) + GetApplication(context.Context, *codedeploy.GetApplicationInput, ...func(*codedeploy.Options)) (*codedeploy.GetApplicationOutput, error) + ListDeployments(context.Context, *codedeploy.ListDeploymentsInput, ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentsOutput, error) + GetDeployment(context.Context, *codedeploy.GetDeploymentInput, ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentOutput, error) + ListDeploymentConfigs(context.Context, *codedeploy.ListDeploymentConfigsInput, ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentConfigsOutput, error) + GetDeploymentConfig(context.Context, *codedeploy.GetDeploymentConfigInput, ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentConfigOutput, error) +} + +func init() { + gob.Register([]string{}) + gob.Register([]codeDeployTypes.ApplicationInfo{}) + gob.Register([]codeDeployTypes.DeploymentConfigInfo{}) + gob.Register([]codeDeployTypes.DeploymentInfo{}) + +} + +func CachedCodeDeployListApplications(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var applications []string + cacheKey := fmt.Sprintf("%s-codedeploy-ListApplications-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]string), nil + } + for { + ListApplications, err := client.ListApplications( + context.TODO(), + &codedeploy.ListApplicationsInput{ + NextToken: PaginationControl, + }, + func(o *codedeploy.Options) { + o.Region = region + }, + ) + + if err != nil { + return applications, err + } + + applications = append(applications, ListApplications.Applications...) + + //pagination + if ListApplications.NextToken == nil { + break + } + PaginationControl = ListApplications.NextToken + } + + internal.Cache.Set(cacheKey, applications, cache.DefaultExpiration) + return applications, nil +} + +func CachedCodeDeployListDeploymentConfigs(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var deploymentConfigs []string + cacheKey := fmt.Sprintf("%s-codedeploy-ListDeploymentConfigs-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]string), nil + } + for { + ListDeploymentConfigs, err := client.ListDeploymentConfigs( + context.TODO(), + &codedeploy.ListDeploymentConfigsInput{ + NextToken: PaginationControl, + }, + func(o *codedeploy.Options) { + o.Region = region + }, + ) + + if err != nil { + return deploymentConfigs, err + } + + deploymentConfigs = append(deploymentConfigs, ListDeploymentConfigs.DeploymentConfigsList...) + + //pagination + if ListDeploymentConfigs.NextToken == nil { + break + } + PaginationControl = ListDeploymentConfigs.NextToken + } + + internal.Cache.Set(cacheKey, deploymentConfigs, cache.DefaultExpiration) + return deploymentConfigs, nil +} + +func CachedCodeDeployListDeployments(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var deployments []string + cacheKey := fmt.Sprintf("%s-codedeploy-ListDeployments-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]string), nil + } + for { + ListDeployments, err := client.ListDeployments( + context.TODO(), + &codedeploy.ListDeploymentsInput{ + NextToken: PaginationControl, + }, + func(o *codedeploy.Options) { + o.Region = region + }, + ) + + if err != nil { + return deployments, err + } + + deployments = append(deployments, ListDeployments.Deployments...) + + //pagination + if ListDeployments.NextToken == nil { + break + } + PaginationControl = ListDeployments.NextToken + } + + internal.Cache.Set(cacheKey, deployments, cache.DefaultExpiration) + return deployments, nil +} + +func CachedCodeDeployGetApplication(client AWSCodeDeployClientInterface, accountID string, region string, applicationName string) (codeDeployTypes.ApplicationInfo, error) { + cacheKey := fmt.Sprintf("%s-codedeploy-GetApplication-%s-%s", accountID, region, applicationName) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.(codeDeployTypes.ApplicationInfo), nil + } + GetApplication, err := client.GetApplication( + context.TODO(), + &codedeploy.GetApplicationInput{ + ApplicationName: &applicationName, + }, + func(o *codedeploy.Options) { + o.Region = region + }, + ) + + if err != nil { + return *GetApplication.Application, err + } + + internal.Cache.Set(cacheKey, *GetApplication.Application, cache.DefaultExpiration) + + return *GetApplication.Application, nil +} + +func CachedCodeDeployGetDeploymentConfig(client AWSCodeDeployClientInterface, accountID string, region string, deploymentConfigName string) (codeDeployTypes.DeploymentConfigInfo, error) { + cacheKey := fmt.Sprintf("%s-codedeploy-GetDeploymentConfig-%s-%s", accountID, region, deploymentConfigName) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.(codeDeployTypes.DeploymentConfigInfo), nil + } + GetDeploymentConfig, err := client.GetDeploymentConfig( + context.TODO(), + &codedeploy.GetDeploymentConfigInput{ + DeploymentConfigName: &deploymentConfigName, + }, + func(o *codedeploy.Options) { + o.Region = region + }, + ) + + if err != nil { + return *GetDeploymentConfig.DeploymentConfigInfo, err + } + + internal.Cache.Set(cacheKey, *GetDeploymentConfig.DeploymentConfigInfo, cache.DefaultExpiration) + + return *GetDeploymentConfig.DeploymentConfigInfo, nil +} + +func CachedCodeDeployGetDeployment(client AWSCodeDeployClientInterface, accountID string, region string, deploymentID string) (codeDeployTypes.DeploymentInfo, error) { + cacheKey := fmt.Sprintf("%s-codedeploy-GetDeployment-%s-%s", accountID, region, deploymentID) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.(codeDeployTypes.DeploymentInfo), nil + } + GetDeployment, err := client.GetDeployment( + context.TODO(), + &codedeploy.GetDeploymentInput{ + DeploymentId: &deploymentID, + }, + func(o *codedeploy.Options) { + o.Region = region + }, + ) + + if err != nil { + return *GetDeployment.DeploymentInfo, err + } + + internal.Cache.Set(cacheKey, *GetDeployment.DeploymentInfo, cache.DefaultExpiration) + + return *GetDeployment.DeploymentInfo, nil +} diff --git a/aws/sdk/codedeploy_mocks.go b/aws/sdk/codedeploy_mocks.go new file mode 100644 index 0000000..f3f0a6e --- /dev/null +++ b/aws/sdk/codedeploy_mocks.go @@ -0,0 +1,85 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/codedeploy" + codedeployTypes "github.com/aws/aws-sdk-go-v2/service/codedeploy/types" +) + +type MockedCodedeployClient struct { +} + +func (m *MockedCodedeployClient) ListApplications(ctx context.Context, input *codedeploy.ListApplicationsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListApplicationsOutput, error) { + return &codedeploy.ListApplicationsOutput{ + Applications: []string{ + "app1", + "app2", + }, + }, nil +} + +func (m *MockedCodedeployClient) ListDeployments(ctx context.Context, input *codedeploy.ListDeploymentsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentsOutput, error) { + return &codedeploy.ListDeploymentsOutput{ + Deployments: []string{ + "deployment1", + "deployment2", + }, + }, nil +} + +func (m *MockedCodedeployClient) ListDeploymentConfigs(ctx context.Context, input *codedeploy.ListDeploymentConfigsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentConfigsOutput, error) { + return &codedeploy.ListDeploymentConfigsOutput{ + DeploymentConfigsList: []string{ + "deploymentConfig1", + "deploymentConfig2", + }, + }, nil +} + +func (m *MockedCodedeployClient) GetApplication(ctx context.Context, input *codedeploy.GetApplicationInput, options ...func(*codedeploy.Options)) (*codedeploy.GetApplicationOutput, error) { + return &codedeploy.GetApplicationOutput{ + Application: &codedeployTypes.ApplicationInfo{ + ApplicationName: aws.String("application1"), + ApplicationId: aws.String("application1"), + CreateTime: aws.Time(time.Now()), + GitHubAccountName: aws.String("github"), + LinkedToGitHub: true, + }, + }, nil +} + +func (m *MockedCodedeployClient) GetDeployment(ctx context.Context, input *codedeploy.GetDeploymentInput, options ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentOutput, error) { + return &codedeploy.GetDeploymentOutput{ + DeploymentInfo: &codedeployTypes.DeploymentInfo{ + ApplicationName: aws.String("application1"), + CreateTime: aws.Time(time.Now()), + DeploymentId: aws.String("deployment1"), + DeploymentOverview: &codedeployTypes.DeploymentOverview{ + Failed: 0, + InProgress: 0, + Pending: 0, + Ready: 0, + Skipped: 0, + Succeeded: 0, + }, + DeploymentConfigName: aws.String("deploymentConfig1"), + Status: codedeployTypes.DeploymentStatusSucceeded, + }, + }, nil +} + +func (m *MockedCodedeployClient) GetDeploymentConfig(ctx context.Context, input *codedeploy.GetDeploymentConfigInput, options ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentConfigOutput, error) { + return &codedeploy.GetDeploymentConfigOutput{ + DeploymentConfigInfo: &codedeployTypes.DeploymentConfigInfo{ + DeploymentConfigName: aws.String("deploymentConfig1"), + ComputePlatform: codedeployTypes.ComputePlatformServer, + MinimumHealthyHosts: &codedeployTypes.MinimumHealthyHosts{ + Type: codedeployTypes.MinimumHealthyHostsTypeFleetPercent, + Value: 100, + }, + }, + }, nil +} diff --git a/aws/sdk/datapipeline.go b/aws/sdk/datapipeline.go new file mode 100644 index 0000000..1c980f9 --- /dev/null +++ b/aws/sdk/datapipeline.go @@ -0,0 +1,56 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/datapipeline" + dataPipelineTypes "github.com/aws/aws-sdk-go-v2/service/datapipeline/types" + "github.com/patrickmn/go-cache" +) + +type AWSDataPipelineClientInterface interface { + ListPipelines(ctx context.Context, input *datapipeline.ListPipelinesInput, opts ...func(*datapipeline.Options)) (*datapipeline.ListPipelinesOutput, error) +} + +func init() { + gob.Register([]dataPipelineTypes.PipelineIdName{}) +} + +func CachedDataPipelineListPipelines(client AWSDataPipelineClientInterface, accountID string, region string) ([]dataPipelineTypes.PipelineIdName, error) { + var PaginationControl *string + var pipelines []dataPipelineTypes.PipelineIdName + cacheKey := fmt.Sprintf("%s-datapipeline-ListPipelines-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]dataPipelineTypes.PipelineIdName), nil + } + for { + ListPipelines, err := client.ListPipelines( + context.TODO(), + &datapipeline.ListPipelinesInput{ + Marker: PaginationControl, + }, + func(o *datapipeline.Options) { + o.Region = region + }, + ) + + if err != nil { + return pipelines, err + } + + pipelines = append(pipelines, ListPipelines.PipelineIdList...) + + //pagination + if ListPipelines.Marker == nil { + break + } + PaginationControl = ListPipelines.Marker + } + + internal.Cache.Set(cacheKey, pipelines, cache.DefaultExpiration) + return pipelines, nil +} diff --git a/aws/sdk/datapipeline_mocks.go b/aws/sdk/datapipeline_mocks.go new file mode 100644 index 0000000..3d1fa61 --- /dev/null +++ b/aws/sdk/datapipeline_mocks.go @@ -0,0 +1,27 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/datapipeline" + dataPipelineTypes "github.com/aws/aws-sdk-go-v2/service/datapipeline/types" +) + +type MockedDataPipelineClient struct { +} + +func (m *MockedDataPipelineClient) ListPipelines(ctx context.Context, input *datapipeline.ListPipelinesInput, options ...func(*datapipeline.Options)) (*datapipeline.ListPipelinesOutput, error) { + return &datapipeline.ListPipelinesOutput{ + PipelineIdList: []dataPipelineTypes.PipelineIdName{ + { + Id: aws.String("pipeline1"), + Name: aws.String("pipeline1"), + }, + { + Id: aws.String("pipeline2"), + Name: aws.String("pipeline2"), + }, + }, + }, nil +} diff --git a/aws/sdk/docdb.go b/aws/sdk/docdb.go index a880f21..e7273fc 100644 --- a/aws/sdk/docdb.go +++ b/aws/sdk/docdb.go @@ -19,7 +19,7 @@ type DocDBClientInterface interface { DescribeDBInstances(context.Context, *docdb.DescribeDBInstancesInput, ...func(*docdb.Options)) (*docdb.DescribeDBInstancesOutput, error) } -func RegisterDocDBTypes() { +func init() { gob.Register([]docdbTypes.GlobalCluster{}) gob.Register([]docdbTypes.DBCluster{}) //gob.Register([]docdbTypes.DBInstance{}) diff --git a/aws/sdk/docsdb_mocks.go b/aws/sdk/docsdb_mocks.go new file mode 100644 index 0000000..64f014e --- /dev/null +++ b/aws/sdk/docsdb_mocks.go @@ -0,0 +1,25 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/docdb" + docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types" +) + +type MockedAWSDocsDBClient struct { +} + +func (m *MockedAWSDocsDBClient) DescribeDBClusters(ctx context.Context, input *docdb.DescribeDBClustersInput, options ...func(*docdb.Options)) (*docdb.DescribeDBClustersOutput, error) { + return &docdb.DescribeDBClustersOutput{ + DBClusters: []docdbTypes.DBCluster{ + { + DBClusterIdentifier: aws.String("cluster1"), + }, + { + DBClusterIdentifier: aws.String("cluster2"), + }, + }, + }, nil +} diff --git a/aws/sdk/dynamodb.go b/aws/sdk/dynamodb.go index cfa4acd..c5e94ec 100644 --- a/aws/sdk/dynamodb.go +++ b/aws/sdk/dynamodb.go @@ -16,7 +16,7 @@ type DynamoDBClientInterface interface { DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) } -func RegisterDynamoDBTypes() { +func init() { gob.Register([]string{}) gob.Register(dynamoDBTypes.TableDescription{}) } diff --git a/aws/sdk/dynamodb_mocks.go b/aws/sdk/dynamodb_mocks.go new file mode 100644 index 0000000..6759d0f --- /dev/null +++ b/aws/sdk/dynamodb_mocks.go @@ -0,0 +1,23 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +type MockedAWSDynamoDBClient struct { +} + +func (m *MockedAWSDynamoDBClient) ListTables() ([]string, error) { + return []string{"table1", "table2"}, nil +} + +func (m *MockedAWSDynamoDBClient) DescribeTable(ctx context.Context, input *dynamodb.DescribeTableInput, options ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) { + return &dynamodb.DescribeTableOutput{ + Table: &dynamodbTypes.TableDescription{ + TableName: input.TableName, + }, + }, nil +} diff --git a/aws/sdk/ec2.go b/aws/sdk/ec2.go index dbdafe2..1deb5ae 100644 --- a/aws/sdk/ec2.go +++ b/aws/sdk/ec2.go @@ -6,20 +6,31 @@ import ( "fmt" "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ec2" ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/patrickmn/go-cache" ) -type EC2ClientInterface interface { +type AWSEC2ClientInterface interface { DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) + DescribeNetworkInterfaces(context.Context, *ec2.DescribeNetworkInterfacesInput, ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) + DescribeSnapshots(context.Context, *ec2.DescribeSnapshotsInput, ...func(*ec2.Options)) (*ec2.DescribeSnapshotsOutput, error) + DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) + DescribeImages(context.Context, *ec2.DescribeImagesInput, ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error) + DescribeInstanceAttribute(context.Context, *ec2.DescribeInstanceAttributeInput, ...func(*ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error) } -func RegisterEC2Types() { +func init() { gob.Register([]ec2Types.Instance{}) + gob.Register([]ec2Types.NetworkInterface{}) + gob.Register([]ec2Types.Snapshot{}) + gob.Register([]ec2Types.Volume{}) + gob.Register([]ec2Types.Image{}) + } -func CachedEC2DescribeInstances(client EC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) { +func CachedEC2DescribeInstances(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) { var PaginationControl *string var instances []ec2Types.Instance cacheKey := fmt.Sprintf("%s-ec2-DescribeInstances-%s", accountID, region) @@ -56,3 +67,162 @@ func CachedEC2DescribeInstances(client EC2ClientInterface, accountID string, reg internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration) return instances, nil } + +func CachedEC2DescribeInstanceAttributeUserData(client AWSEC2ClientInterface, accountID string, region string, instanceID string) (string, error) { + cacheKey := fmt.Sprintf("%s-ec2-DescribeInstanceAttributeUserData-%s-%s", accountID, region, instanceID) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.(string), nil + } + DescribeInstanceAttribute, err := client.DescribeInstanceAttribute( + context.TODO(), + &ec2.DescribeInstanceAttributeInput{ + Attribute: ec2Types.InstanceAttributeNameUserData, + InstanceId: &instanceID, + }, + func(o *ec2.Options) { + o.Region = region + }, + ) + if err != nil { + return "", err + } + internal.Cache.Set(cacheKey, aws.ToString(DescribeInstanceAttribute.UserData.Value), cache.DefaultExpiration) + + return aws.ToString(DescribeInstanceAttribute.UserData.Value), nil +} + +func CachedEC2DescribeNetworkInterfaces(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.NetworkInterface, error) { + var PaginationControl *string + var NetworkInterfaces []ec2Types.NetworkInterface + cacheKey := fmt.Sprintf("%s-ec2-DescribeNetworkInterfaces-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]ec2Types.NetworkInterface), nil + } + for { + DescribeNetworkInterfaces, err := client.DescribeNetworkInterfaces( + context.TODO(), + &(ec2.DescribeNetworkInterfacesInput{ + NextToken: PaginationControl, + }), + func(o *ec2.Options) { + o.Region = region + }, + ) + if err != nil { + return NetworkInterfaces, err + } + NetworkInterfaces = append(NetworkInterfaces, DescribeNetworkInterfaces.NetworkInterfaces...) + + if DescribeNetworkInterfaces.NextToken == nil { + break + } + PaginationControl = DescribeNetworkInterfaces.NextToken + } + + internal.Cache.Set(cacheKey, NetworkInterfaces, cache.DefaultExpiration) + return NetworkInterfaces, nil +} + +func CachedEC2DescribeSnapshots(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Snapshot, error) { + var PaginationControl *string + var Snapshots []ec2Types.Snapshot + cacheKey := fmt.Sprintf("%s-ec2-DescribeSnapshots-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]ec2Types.Snapshot), nil + } + for { + DescribeSnapshots, err := client.DescribeSnapshots( + context.TODO(), + &(ec2.DescribeSnapshotsInput{ + NextToken: PaginationControl, + OwnerIds: []string{accountID}, + }), + func(o *ec2.Options) { + o.Region = region + }, + ) + if err != nil { + return Snapshots, err + } + Snapshots = append(Snapshots, DescribeSnapshots.Snapshots...) + + if DescribeSnapshots.NextToken == nil { + break + } + PaginationControl = DescribeSnapshots.NextToken + } + + internal.Cache.Set(cacheKey, Snapshots, cache.DefaultExpiration) + return Snapshots, nil +} + +func CachedEC2DescribeVolumes(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Volume, error) { + var PaginationControl *string + var Volumes []ec2Types.Volume + cacheKey := fmt.Sprintf("%s-ec2-DescribeVolumes-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]ec2Types.Volume), nil + } + for { + DescribeVolumes, err := client.DescribeVolumes( + context.TODO(), + &(ec2.DescribeVolumesInput{ + NextToken: PaginationControl, + }), + func(o *ec2.Options) { + o.Region = region + }, + ) + if err != nil { + return Volumes, err + } + Volumes = append(Volumes, DescribeVolumes.Volumes...) + + if DescribeVolumes.NextToken == nil { + break + } + PaginationControl = DescribeVolumes.NextToken + } + + internal.Cache.Set(cacheKey, Volumes, cache.DefaultExpiration) + return Volumes, nil +} + +func CachedEC2DescribeImages(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Image, error) { + var PaginationControl *string + var Images []ec2Types.Image + cacheKey := fmt.Sprintf("%s-ec2-DescribeImages-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]ec2Types.Image), nil + } + for { + DescribeImages, err := client.DescribeImages( + context.TODO(), + &(ec2.DescribeImagesInput{ + Owners: []string{accountID}, + NextToken: PaginationControl, + }), + func(o *ec2.Options) { + o.Region = region + }, + ) + if err != nil { + return Images, err + } + Images = append(Images, DescribeImages.Images...) + + if DescribeImages.NextToken == nil { + break + } + PaginationControl = DescribeImages.NextToken + } + + internal.Cache.Set(cacheKey, Images, cache.DefaultExpiration) + return Images, nil + +} diff --git a/aws/sdk/ec2_mocks.go b/aws/sdk/ec2_mocks.go new file mode 100644 index 0000000..51546cf --- /dev/null +++ b/aws/sdk/ec2_mocks.go @@ -0,0 +1,388 @@ +package sdk + +// create test for the Cached EC2 functions. Create a mocked client and mocked functions for each of the methods used in the ec2.go file + +import ( + "context" + "encoding/json" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +const DESCRIBE_NETWORK_INTEFACES_TEST_FILE = "./test-data/describe-network-interfaces.json" +const DESCRIBE_INSTANCES_TEST_FILE = "./test-data/ec2-describeInstances.json" +const DESCRIBE_SNAPSHOTS_TEST_FILE = "./test-data/ec2-describeSnapshots.json" +const DESCRIBE_VOLUMES_TEST_FILE = "./test-data/ec2-describeVolumes.json" +const DESCRIBE_IMAGES_TEST_FILE = "./test-data/ec2-describeImages.json" + +type MockedEC2Client2 struct { + describeNetworkInterfaces DescribeNetworkInterfaces + describeInstances DescribeInstances + describeSnapshots DescribeSnapshots + describeVolumes DescribeVolumes + describeImages DescribeImages +} + +type DescribeNetworkInterfaces struct { + NetworkInterfaces []struct { + Status string `json:"Status"` + MacAddress string `json:"MacAddress"` + SourceDestCheck bool `json:"SourceDestCheck"` + VpcID string `json:"VpcId"` + Description string `json:"Description"` + Association struct { + PublicIP string `json:"PublicIp"` + AssociationID string `json:"AssociationId"` + PublicDNSName string `json:"PublicDnsName"` + IPOwnerID string `json:"IpOwnerId"` + } `json:"Association"` + NetworkInterfaceID string `json:"NetworkInterfaceId"` + PrivateIPAddresses []struct { + PrivateDNSName string `json:"PrivateDnsName"` + Association struct { + PublicIP string `json:"PublicIp"` + AssociationID string `json:"AssociationId"` + PublicDNSName string `json:"PublicDnsName"` + IPOwnerID string `json:"IpOwnerId"` + } `json:"Association"` + Primary bool `json:"Primary"` + PrivateIPAddress string `json:"PrivateIpAddress"` + } `json:"PrivateIpAddresses"` + RequesterManaged bool `json:"RequesterManaged"` + Ipv6Addresses []interface{} `json:"Ipv6Addresses"` + PrivateDNSName string `json:"PrivateDnsName,omitempty"` + AvailabilityZone string `json:"AvailabilityZone"` + Attachment struct { + Status string `json:"Status"` + DeviceIndex int `json:"DeviceIndex"` + AttachTime time.Time `json:"AttachTime"` + InstanceID string `json:"InstanceId"` + DeleteOnTermination bool `json:"DeleteOnTermination"` + AttachmentID string `json:"AttachmentId"` + InstanceOwnerID string `json:"InstanceOwnerId"` + } `json:"Attachment"` + Groups []struct { + GroupName string `json:"GroupName"` + GroupID string `json:"GroupId"` + } `json:"Groups"` + SubnetID string `json:"SubnetId"` + OwnerID string `json:"OwnerId"` + TagSet []interface{} `json:"TagSet"` + PrivateIPAddress string `json:"PrivateIpAddress"` + } `json:"NetworkInterfaces"` +} + +type DescribeInstances struct { + Reservations []struct { + Instances []struct { + AmiLaunchIndex int `json:"AmiLaunchIndex"` + ImageID string `json:"ImageId"` + InstanceID string `json:"InstanceId"` + InstanceType string `json:"InstanceType"` + KernelID string `json:"KernelId"` + KeyName string `json:"KeyName"` + LaunchTime string `json:"LaunchTime"` + Monitoring struct { + State string `json:"State"` + } `json:"Monitoring"` + Placement struct { + AvailabilityZone string `json:"AvailabilityZone"` + GroupName string `json:"GroupName"` + Tenancy string `json:"Tenancy"` + } `json:"Placement"` + Platform string `json:"Platform"` + PrivateDNS string `json:"PrivateDnsName"` + PrivateIP string `json:"PrivateIpAddress"` + PublicDNS string `json:"PublicDnsName"` + PublicIP string `json:"PublicIpAddress"` + State string `json:"State"` + SubnetID string `json:"SubnetId"` + VpcID string `json:"VpcId"` + Architecture string `json:"Architecture"` + BlockDeviceMappings []struct { + DeviceName string `json:"DeviceName"` + Ebs struct { + AttachTime time.Time `json:"AttachTime"` + DeleteOnTermination bool `json:"DeleteOnTermination"` + Status string `json:"Status"` + VolumeID string `json:"VolumeId"` + } `json:"Ebs"` + } `json:"BlockDeviceMappings"` + ClientToken string `json:"ClientToken"` + EbsOptimized bool `json:"EbsOptimized"` + Hypervisor string `json:"Hypervisor"` + IamInstanceProfile struct { + Arn string `json:"Arn"` + ID string `json:"Id"` + } `json:"IamInstanceProfile"` + NetworkInterfaces []struct { + Attachment struct { + AttachTime time.Time `json:"AttachTime"` + AttachmentID string `json:"AttachmentId"` + DeleteOnTermination bool `json:"DeleteOnTermination"` + DeviceIndex int `json:"DeviceIndex"` + Status string `json:"Status"` + } `json:"Attachment"` + Description string `json:"Description"` + Groups []struct { + GroupName string `json:"GroupName"` + GroupID string `json:"GroupId"` + } `json:"Groups"` + MacAddress string `json:"MacAddress"` + NetworkInterfaceID string `json:"NetworkInterfaceId"` + OwnerID string `json:"OwnerId"` + PrivateDNSName string `json:"PrivateDnsName"` + PrivateIPAddress string `json:"PrivateIpAddress"` + PrivateIPAddresses []struct { + Association struct { + IPOwnerID string `json:"IpOwnerId"` + PublicDNSName string `json:"PublicDnsName"` + PublicIP string `json:"PublicIp"` + } `json:"Association"` + Primary bool `json:"Primary"` + PrivateDNSName string `json:"PrivateDnsName"` + PrivateIPAddress string `json:"PrivateIpAddress"` + } `json:"PrivateIpAddresses"` + SourceDestCheck bool `json:"SourceDestCheck"` + Status string `json:"Status"` + SubnetID string `json:"SubnetId"` + VpcID string `json:"VpcId"` + } `json:"NetworkInterfaces"` + RootDeviceName string `json:"RootDeviceName"` + RootDeviceType string `json:"RootDeviceType"` + SecurityGroups []struct { + GroupName string `json:"GroupName"` + GroupID string `json:"GroupId"` + } `json:"SecurityGroups"` + SourceDestCheck bool `json:"SourceDestCheck"` + StateReason struct { + Code string `json:"Code"` + Message string `json:"Message"` + } `json:"StateReason"` + Tags []struct { + Key string `json:"Key"` + Value string `json:"Value"` + } `json:"Tags"` + VirtualizationType string `json:"VirtualizationType"` + } `json:"Instances"` + OwnerID string `json:"OwnerId"` + RequesterID string `json:"RequesterId"` + ReservationID string `json:"ReservationId"` + } `json:"Reservations"` +} + +type DescribeImages struct { + Images []struct { + Architecture string `json:"Architecture"` + BlockDeviceMappings []struct { + DeviceName string `json:"DeviceName"` + Ebs struct { + DeleteOnTermination bool `json:"DeleteOnTermination"` + SnapshotID string `json:"SnapshotId"` + VolumeSize int `json:"VolumeSize"` + VolumeType string `json:"VolumeType"` + } `json:"Ebs"` + } `json:"BlockDeviceMappings"` + CreationDate string `json:"CreationDate"` + Description string `json:"Description"` + EnaSupport bool `json:"EnaSupport"` + Hypervisor string `json:"Hypervisor"` + ImageID string `json:"ImageId"` + ImageLocation string `json:"ImageLocation"` + ImageType string `json:"ImageType"` + KernelID string `json:"KernelId"` + Name string `json:"Name"` + OwnerAlias string `json:"OwnerAlias"` + OwnerID string `json:"OwnerId"` + Platform string `json:"Platform"` + ProductCodes []struct { + ProductCodeID string `json:"ProductCodeId"` + ProductCodeType string `json:"ProductCodeType"` + } `json:"ProductCodes"` + Public bool `json:"Public"` + RamdiskID string `json:"RamdiskId"` + RootDeviceName string `json:"RootDeviceName"` + RootDeviceType string `json:"RootDeviceType"` + SriovNetSupport string `json:"SriovNetSupport"` + State string `json:"State"` + StateReason struct { + Code string `json:"Code"` + Message string `json:"Message"` + } `json:"StateReason"` + Tags []struct { + Key string `json:"Key"` + Value string `json:"Value"` + } `json:"Tags"` + VirtualizationType string `json:"VirtualizationType"` + } `json:"Images"` + NextToken string `json:"NextToken"` +} + +type DescribeSnapshots struct { + Snapshots []struct { + DataEncryptionKeyID string `json:"DataEncryptionKeyId"` + Description string `json:"Description"` + Encrypted bool `json:"Encrypted"` + KMSKeyID string `json:"KmsKeyId"` + OwnerAlias string `json:"OwnerAlias"` + OwnerID string `json:"OwnerId"` + Progress string `json:"Progress"` + SnapshotID string `json:"SnapshotId"` + StartTime time.Time `json:"StartTime"` + State string `json:"State"` + StateMessage string `json:"StateMessage"` + TagSet []struct { + Key string `json:"Key"` + Value string `json:"Value"` + } `json:"Tags"` + VolumeID string `json:"VolumeId"` + VolumeSize int `json:"VolumeSize"` + } `json:"Snapshots"` +} + +type DescribeVolumes struct { + Volumes []struct { + Attachments []struct { + AttachTime time.Time `json:"AttachTime"` + DeleteOnTermination bool `json:"DeleteOnTermination"` + Device string `json:"Device"` + InstanceID string `json:"InstanceId"` + State string `json:"State"` + VolumeID string `json:"VolumeId"` + } `json:"Attachments"` + AvailabilityZone string `json:"AvailabilityZone"` + CreateTime string `json:"CreateTime"` + Encrypted bool `json:"Encrypted"` + KMSKeyID string `json:"KmsKeyId"` + Size int `json:"Size"` + SnapshotID string `json:"SnapshotId"` + State string `json:"State"` + VolumeID string `json:"VolumeId"` + } `json:"Volumes"` +} + +// DescribeInstanceAttribute is a struct for the DescribeInstanceAttribute function - include userdata +type DescribeInstanceAttribute struct { + UserData string `json:"UserData"` + InstanceID string `json:"InstanceId"` +} + +func (c *MockedEC2Client2) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) { + var nics []ec2types.NetworkInterface + err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces) + if err != nil { + log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE) + } + for _, mockednic := range c.describeNetworkInterfaces.NetworkInterfaces { + nics = append(nics, ec2types.NetworkInterface{ + Association: &ec2types.NetworkInterfaceAssociation{ + PublicIp: aws.String(mockednic.Association.PublicIP), + }, + NetworkInterfaceId: aws.String(mockednic.NetworkInterfaceID), + PrivateIpAddress: aws.String(mockednic.PrivateIPAddress), + VpcId: aws.String(mockednic.VpcID), + Attachment: &ec2types.NetworkInterfaceAttachment{InstanceId: aws.String(mockednic.Attachment.InstanceID)}, + Description: aws.String(mockednic.Description), + }) + } + return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil +} + +func (c *MockedEC2Client2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, f ...func(o *ec2.Options)) (*ec2.DescribeInstancesOutput, error) { + var instances []ec2types.Instance + err := json.Unmarshal(readTestFile(DESCRIBE_INSTANCES_TEST_FILE), &c.describeInstances) + if err != nil { + log.Fatalf("can't unmarshall file %s", DESCRIBE_INSTANCES_TEST_FILE) + } + for _, mockedReservation := range c.describeInstances.Reservations { + for _, mockedInstance := range mockedReservation.Instances { + for _, inputInstanceID := range input.InstanceIds { + if mockedInstance.InstanceID == inputInstanceID { + instances = append(instances, ec2types.Instance{ + InstanceId: aws.String(mockedInstance.InstanceID), + InstanceType: ec2types.InstanceType(mockedInstance.InstanceType), + ImageId: aws.String(mockedInstance.ImageID), + }) + } + } + } + } + return &ec2.DescribeInstancesOutput{Reservations: []ec2types.Reservation{ + {Instances: instances}, + }}, nil +} + +func (c *MockedEC2Client2) DescribeVolumes(ctx context.Context, input *ec2.DescribeVolumesInput, f ...func(o *ec2.Options)) (*ec2.DescribeVolumesOutput, error) { + var volumes []ec2types.Volume + err := json.Unmarshal(readTestFile(DESCRIBE_VOLUMES_TEST_FILE), &c.describeVolumes) + if err != nil { + log.Fatalf("can't unmarshall file %s", DESCRIBE_VOLUMES_TEST_FILE) + } + for _, mockedVolume := range c.describeVolumes.Volumes { + for _, inputVolumeID := range input.VolumeIds { + if mockedVolume.VolumeID == inputVolumeID { + volumes = append(volumes, ec2types.Volume{ + VolumeId: aws.String(mockedVolume.VolumeID), + Size: aws.Int32(int32(mockedVolume.Size)), + Attachments: []ec2types.VolumeAttachment{ + { + InstanceId: aws.String(mockedVolume.Attachments[0].InstanceID), + }, + }, + }) + } + } + } + return &ec2.DescribeVolumesOutput{Volumes: volumes}, nil +} + +func (c *MockedEC2Client2) DescribeSnapshots(ctx context.Context, input *ec2.DescribeSnapshotsInput, f ...func(o *ec2.Options)) (*ec2.DescribeSnapshotsOutput, error) { + var snapshots []ec2types.Snapshot + err := json.Unmarshal(readTestFile(DESCRIBE_SNAPSHOTS_TEST_FILE), &c.describeSnapshots) + if err != nil { + log.Fatalf("can't unmarshall file %s", DESCRIBE_SNAPSHOTS_TEST_FILE) + } + for _, mockedSnapshot := range c.describeSnapshots.Snapshots { + for _, inputSnapshotID := range input.SnapshotIds { + if mockedSnapshot.SnapshotID == inputSnapshotID { + snapshots = append(snapshots, ec2types.Snapshot{ + SnapshotId: aws.String(mockedSnapshot.SnapshotID), + VolumeId: aws.String(mockedSnapshot.VolumeID), + State: ec2types.SnapshotState(mockedSnapshot.State), + }) + } + } + } + return &ec2.DescribeSnapshotsOutput{Snapshots: snapshots}, nil +} + +func (c *MockedEC2Client2) DescribeImages(ctx context.Context, input *ec2.DescribeImagesInput, f ...func(o *ec2.Options)) (*ec2.DescribeImagesOutput, error) { + var images []ec2types.Image + err := json.Unmarshal(readTestFile(DESCRIBE_IMAGES_TEST_FILE), &c.describeImages) + if err != nil { + log.Fatalf("can't unmarshall file %s", DESCRIBE_IMAGES_TEST_FILE) + } + for _, mockedImage := range c.describeImages.Images { + for _, inputImageID := range input.ImageIds { + if mockedImage.ImageID == inputImageID { + images = append(images, ec2types.Image{ + ImageId: aws.String(mockedImage.ImageID), + Name: aws.String(mockedImage.Name), + }) + } + } + } + return &ec2.DescribeImagesOutput{Images: images}, nil +} + +func (c *MockedEC2Client2) DescribeInstanceAttribute(ctx context.Context, input *ec2.DescribeInstanceAttributeInput, f ...func(o *ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error) { + return &ec2.DescribeInstanceAttributeOutput{ + UserData: &ec2types.AttributeValue{ + Value: aws.String("userdata"), + }, + }, nil +} diff --git a/aws/sdk/ecr.go b/aws/sdk/ecr.go index 492c38d..75a265d 100644 --- a/aws/sdk/ecr.go +++ b/aws/sdk/ecr.go @@ -19,9 +19,10 @@ type AWSECRClientInterface interface { GetRepositoryPolicy(ctx context.Context, params *ecr.GetRepositoryPolicyInput, optFns ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) } -func RegisterECRTypes() { +func init() { gob.Register([]ecrTypes.Repository{}) gob.Register([]ecrTypes.ImageDetail{}) + gob.Register(ecrTypes.Repository{}) } @@ -69,7 +70,7 @@ func CachedECRDescribeRepositories(ECRClient AWSECRClientInterface, accountID st func CachedECRDescribeImages(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) ([]ecrTypes.ImageDetail, error) { var PaginationControl *string var images []ecrTypes.ImageDetail - cacheKey := fmt.Sprintf("%s-efs-DescribImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-")) + cacheKey := fmt.Sprintf("%s-ecr-DescribeImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-")) cached, found := internal.Cache.Get(cacheKey) if found { sharedLogger.Debug("Using cached Images data") diff --git a/aws/sdk/ecr_mocks.go b/aws/sdk/ecr_mocks.go new file mode 100644 index 0000000..f937c82 --- /dev/null +++ b/aws/sdk/ecr_mocks.go @@ -0,0 +1,109 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecr" + ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types" +) + +type MockedECRClient struct { +} + +func (m *MockedECRClient) DescribeRepositories(ctx context.Context, input *ecr.DescribeRepositoriesInput, options ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) { + return &ecr.DescribeRepositoriesOutput{ + Repositories: []ecrTypes.Repository{ + { + RepositoryName: aws.String("repo1"), + RepositoryUri: aws.String("11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo1"), + }, + { + RepositoryName: aws.String("repo2"), + RepositoryUri: aws.String("11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo2"), + }, + }, + }, nil +} + +func (m *MockedECRClient) DescribeImages(ctx context.Context, input *ecr.DescribeImagesInput, options ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) { + if aws.ToString(input.RepositoryName) == "repo1" { + return &ecr.DescribeImagesOutput{ + ImageDetails: []ecrTypes.ImageDetail{ + { + ImageTags: []string{ + "customtag", + "tag2", + }, + ImagePushedAt: aws.Time(time.Date(2022, 10, 25, 15, 14, 0, 0, time.UTC)), + ImageSizeInBytes: aws.Int64(123456), + }, + }, + }, nil + } else if aws.ToString(input.RepositoryName) == "repo2" { + return &ecr.DescribeImagesOutput{ + ImageDetails: []ecrTypes.ImageDetail{ + { + ImageTags: []string{ + "latest", + }, + ImagePushedAt: aws.Time(time.Date(2021, 10, 15, 11, 14, 0, 0, time.UTC)), + ImageSizeInBytes: aws.Int64(2222222), + }, + }, + }, nil + } else { + return &ecr.DescribeImagesOutput{ + ImageDetails: []ecrTypes.ImageDetail{ + { + ImageTags: []string{ + "customtag", + "tag2", + }, + ImagePushedAt: aws.Time(time.Date(2022, 10, 25, 15, 14, 0, 0, time.UTC)), + ImageSizeInBytes: aws.Int64(111), + }, + { + ImageTags: []string{ + "latest", + }, + ImagePushedAt: aws.Time(time.Date(2021, 10, 15, 11, 14, 0, 0, time.UTC)), + ImageSizeInBytes: aws.Int64(333), + }, + }, + }, nil + } + +} + +func (m *MockedECRClient) GetRepositoryPolicy(ctx context.Context, input *ecr.GetRepositoryPolicyInput, options ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) { + return &ecr.GetRepositoryPolicyOutput{ + PolicyText: aws.String(`{ + "Version": "2008-10-17", + "Statement": [ + { + "Sid": "AllowPushPull", + "Effect": "Allow", + "Principal": { + "AWS": [ + "arn:aws:iam::123456789012:root", + "arn:aws:iam::123456789012:user/MyUser" + ] + }, + "Action": [ + + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload" + ] + + } + ] + }`), + }, nil +} diff --git a/aws/sdk/ecs.go b/aws/sdk/ecs.go new file mode 100644 index 0000000..049d490 --- /dev/null +++ b/aws/sdk/ecs.go @@ -0,0 +1,222 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + "strings" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "github.com/patrickmn/go-cache" +) + +type AWSECSClientInterface interface { + ListClusters(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) + ListTasks(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) + ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) + DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) + DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) +} + +func init() { + gob.Register([]string{}) + gob.Register(ecsTypes.Task{}) + gob.Register([]ecsTypes.Task{}) + gob.Register(ecsTypes.TaskDefinition{}) + +} + +func CachedECSListClusters(ECSClient AWSECSClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var clusters []string + cacheKey := fmt.Sprintf("%s-ecs-ListClusters-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached ECS clusters data") + return cached.([]string), nil + } + + for { + ListClusters, err := ECSClient.ListClusters( + context.TODO(), + &ecs.ListClustersInput{ + NextToken: PaginationControl, + }, + func(o *ecs.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + clusters = append(clusters, ListClusters.ClusterArns...) + + // Pagination control. + if ListClusters.NextToken != nil { + PaginationControl = ListClusters.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration) + return clusters, nil +} + +func CachedECSListTasks(ECSClient AWSECSClientInterface, accountID string, region string, cluster string) ([]string, error) { + var PaginationControl *string + var tasks []string + //grab cluster name from AWS ARN + clusterName := cluster[strings.LastIndex(cluster, "/")+1:] + + cacheKey := fmt.Sprintf("%s-ecs-ListTasks-%s-%s", accountID, region, clusterName) + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached ECS tasks data") + return cached.([]string), nil + } + + for { + ListTasks, err := ECSClient.ListTasks( + context.TODO(), + &ecs.ListTasksInput{ + Cluster: &cluster, + NextToken: PaginationControl, + }, + func(o *ecs.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + tasks = append(tasks, ListTasks.TaskArns...) + + // Pagination control. + if ListTasks.NextToken != nil { + PaginationControl = ListTasks.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, tasks, cache.DefaultExpiration) + return tasks, nil +} + +func CachedECSDescribeTasks(ECSClient AWSECSClientInterface, accountID string, region string, cluster string, tasks []string) ([]ecsTypes.Task, error) { + var taskDetails []ecsTypes.Task + //replace semi-colons with underscores in task definition name + clusterFileSystemSafe := strings.ReplaceAll(cluster, ":", "_") + clusterFileSystemSafe = strings.ReplaceAll(clusterFileSystemSafe, "/", "_") + cacheKey := fmt.Sprintf("%s-ecs-DescribeTasks-%s-%s", accountID, region, clusterFileSystemSafe) + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached ECS task details data") + return cached.([]ecsTypes.Task), nil + } + + DescribeTasks, err := ECSClient.DescribeTasks( + context.TODO(), + &ecs.DescribeTasksInput{ + Cluster: &cluster, + Tasks: tasks, + }, + func(o *ecs.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + return []ecsTypes.Task{}, err + } + + taskDetails = append(taskDetails, DescribeTasks.Tasks...) + + internal.Cache.Set(cacheKey, taskDetails, cache.DefaultExpiration) + return taskDetails, nil +} + +func CachedECSDescribeTaskDefinition(ECSClient AWSECSClientInterface, accountID string, region string, taskDefinition string) (ecsTypes.TaskDefinition, error) { + var taskDefinitionDetails ecsTypes.TaskDefinition + //replace semi-colons with underscores in task definition name + taskDefinitionFileSystemSafe := strings.ReplaceAll(taskDefinition, ":", "_") + taskDefinitionFileSystemSafe = strings.ReplaceAll(taskDefinitionFileSystemSafe, "/", "_") + cacheKey := fmt.Sprintf("%s-ecs-DescribeTaskDefinition-%s-%s", accountID, region, taskDefinitionFileSystemSafe) + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached ECS task definition details data") + return cached.(ecsTypes.TaskDefinition), nil + } + + DescribeTaskDefinition, err := ECSClient.DescribeTaskDefinition( + context.TODO(), + &ecs.DescribeTaskDefinitionInput{ + TaskDefinition: &taskDefinition, + }, + func(o *ecs.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + return ecsTypes.TaskDefinition{}, err + } + + taskDefinitionDetails = *DescribeTaskDefinition.TaskDefinition + + internal.Cache.Set(cacheKey, taskDefinitionDetails, cache.DefaultExpiration) + return taskDefinitionDetails, nil +} + +func CachedECSListServices(ECSClient AWSECSClientInterface, accountID string, region string, cluster string) ([]string, error) { + var PaginationControl *string + var services []string + //grab cluster name from AWS ARN + clusterName := cluster[strings.LastIndex(cluster, "/")+1:] + + cacheKey := fmt.Sprintf("%s-ecs-ListServices-%s-%s", accountID, region, clusterName) + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached ECS services data") + return cached.([]string), nil + } + + for { + ListServices, err := ECSClient.ListServices( + context.TODO(), + &ecs.ListServicesInput{ + Cluster: &cluster, + NextToken: PaginationControl, + }, + func(o *ecs.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + services = append(services, ListServices.ServiceArns...) + + // Pagination control. + if ListServices.NextToken != nil { + PaginationControl = ListServices.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, services, cache.DefaultExpiration) + return services, nil +} diff --git a/aws/sdk/ecs_mocks.go b/aws/sdk/ecs_mocks.go new file mode 100644 index 0000000..9ee5a22 --- /dev/null +++ b/aws/sdk/ecs_mocks.go @@ -0,0 +1,156 @@ +package sdk + +import ( + "context" + "encoding/json" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" +) + +const DESCRIBE_TASKS_TEST_FILE = "./test-data/describe-tasks.json" + +type MockedECSClient struct { + describeTasks DescribeTasks +} + +type ListTasks struct { + TaskArns []string `json:"taskArns"` +} + +type DescribeTasks struct { + Tasks []struct { + Attachments []struct { + ID string `json:"id"` + Type string `json:"type"` + Status string `json:"status"` + Details []struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"details"` + } `json:"attachments"` + Attributes []struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"attributes"` + AvailabilityZone string `json:"availabilityZone"` + ClusterArn string `json:"clusterArn"` + Connectivity string `json:"connectivity"` + ConnectivityAt string `json:"connectivityAt"` + Containers []struct { + ContainerArn string `json:"containerArn"` + TaskArn string `json:"taskArn"` + Name string `json:"name"` + Image string `json:"image"` + RuntimeID string `json:"runtimeId"` + LastStatus string `json:"lastStatus"` + NetworkBindings []interface{} `json:"networkBindings"` + NetworkInterfaces []struct { + AttachmentID string `json:"attachmentId"` + PrivateIpv4Address string `json:"privateIpv4Address"` + } `json:"networkInterfaces"` + HealthStatus string `json:"healthStatus"` + CPU string `json:"cpu"` + Memory string `json:"memory"` + } `json:"containers"` + CPU string `json:"cpu"` + CreatedAt string `json:"createdAt"` + DesiredStatus string `json:"desiredStatus"` + EnableExecuteCommand bool `json:"enableExecuteCommand"` + Group string `json:"group"` + HealthStatus string `json:"healthStatus"` + LastStatus string `json:"lastStatus"` + LaunchType string `json:"launchType"` + Memory string `json:"memory"` + Overrides struct { + ContainerOverrides []struct { + Name string `json:"name"` + } `json:"containerOverrides"` + InferenceAcceleratorOverrides []interface{} `json:"inferenceAcceleratorOverrides"` + } `json:"overrides"` + PlatformVersion string `json:"platformVersion"` + PlatformFamily string `json:"platformFamily"` + PullStartedAt string `json:"pullStartedAt"` + PullStoppedAt string `json:"pullStoppedAt"` + StartedAt string `json:"startedAt"` + StartedBy string `json:"startedBy"` + Tags []interface{} `json:"tags"` + TaskArn string `json:"taskArn"` + TaskDefinitionArn string `json:"taskDefinitionArn"` + Version int `json:"version"` + EphemeralStorage struct { + SizeInGiB int `json:"sizeInGiB"` + } `json:"ephemeralStorage"` + } `json:"tasks"` + Failures []interface{} `json:"failures"` +} + +func (c *MockedECSClient) ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) { + return &ecs.ListClustersOutput{ClusterArns: []string{ + "arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster", + "arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster2", + "arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster3", + }}, nil +} + +func (c *MockedECSClient) ListTasks(ctx context.Context, input *ecs.ListTasksInput, f ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) { + return &ecs.ListTasksOutput{TaskArns: []string{ + "arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE", + "arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE", + }}, nil +} + +func (c *MockedECSClient) ListServices(ctx context.Context, input *ecs.ListServicesInput, f ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) { + return &ecs.ListServicesOutput{ServiceArns: []string{ + "arn:aws:ecs:us-east-1:123456789012:service/MyService", + "arn:aws:ecs:us-east-1:123456789012:service/MyService2", + "arn:aws:ecs:us-east-1:123456789012:service/MyService3", + }}, nil +} + +func (c *MockedECSClient) DescribeTasks(ctx context.Context, input *ecs.DescribeTasksInput, f ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { + err := json.Unmarshal(readTestFile(DESCRIBE_TASKS_TEST_FILE), &c.describeTasks) + if err != nil { + log.Fatalf("can't unmarshall file %s", DESCRIBE_TASKS_TEST_FILE) + } + var tasks []ecsTypes.Task + for _, mockedTask := range c.describeTasks.Tasks { + if mockedTask.ClusterArn == aws.ToString(input.Cluster) { + for _, inputTask := range input.Tasks { + if mockedTask.TaskArn == inputTask { + var attachments []ecsTypes.Attachment + for _, a := range mockedTask.Attachments { + var deets []ecsTypes.KeyValuePair + for _, detail := range a.Details { + deets = append(deets, ecsTypes.KeyValuePair{ + Name: aws.String(detail.Name), + Value: aws.String(detail.Value)}) + } + attachments = append(attachments, ecsTypes.Attachment{ + Type: aws.String(a.Type), + Details: deets, + Id: aws.String(a.ID), + Status: aws.String(a.Status), + }) + } + tasks = append(tasks, ecsTypes.Task{ + ClusterArn: aws.String(mockedTask.ClusterArn), + TaskDefinitionArn: aws.String(mockedTask.TaskDefinitionArn), + LaunchType: ecsTypes.LaunchType(*aws.String(mockedTask.LaunchType)), + TaskArn: aws.String(mockedTask.TaskArn), + Attachments: attachments, + }) + } + } + } + } + return &ecs.DescribeTasksOutput{Tasks: tasks}, nil +} + +func (c *MockedECSClient) DescribeTaskDefinition(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, f ...func(o *ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { + testTaskDefinition := ecsTypes.TaskDefinition{} + testTaskDefinition.TaskRoleArn = aws.String("test123") + return &ecs.DescribeTaskDefinitionOutput{TaskDefinition: &testTaskDefinition}, nil +} diff --git a/aws/sdk/efs.go b/aws/sdk/efs.go index 9146621..9a0c6ee 100644 --- a/aws/sdk/efs.go +++ b/aws/sdk/efs.go @@ -20,7 +20,7 @@ type AWSEFSClientInterface interface { DescribeFileSystemPolicy(ctx context.Context, params *efs.DescribeFileSystemPolicyInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemPolicyOutput, error) } -func RegisterEFSTypes() { +func init() { gob.Register([]efsTypes.FileSystemDescription{}) gob.Register([]efsTypes.MountTargetDescription{}) gob.Register([]efsTypes.AccessPointDescription{}) diff --git a/aws/sdk/efs_mocks.go b/aws/sdk/efs_mocks.go new file mode 100644 index 0000000..d7462ae --- /dev/null +++ b/aws/sdk/efs_mocks.go @@ -0,0 +1,67 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/efs" + efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types" +) + +type MockedEfsClient struct { +} + +func (m *MockedEfsClient) DescribeFileSystems(ctx context.Context, input *efs.DescribeFileSystemsInput, options ...func(*efs.Options)) (*efs.DescribeFileSystemsOutput, error) { + return &efs.DescribeFileSystemsOutput{ + FileSystems: []efsTypes.FileSystemDescription{ + { + FileSystemId: aws.String("fs-12345678"), + }, + { + FileSystemId: aws.String("fs-87654321"), + }, + }, + }, nil +} + +func (m *MockedEfsClient) DescribeMountTargets(ctx context.Context, input *efs.DescribeMountTargetsInput, options ...func(*efs.Options)) (*efs.DescribeMountTargetsOutput, error) { + return &efs.DescribeMountTargetsOutput{ + MountTargets: []efsTypes.MountTargetDescription{ + { + MountTargetId: aws.String("fsmt-12345678"), + FileSystemId: aws.String("fs-12345678"), + IpAddress: aws.String("10.1.1.1"), + }, + { + MountTargetId: aws.String("fsmt-87654321"), + FileSystemId: aws.String("fs-87654321"), + IpAddress: aws.String("10.2.2.2.2"), + }, + }, + }, nil +} + +func (m *MockedEfsClient) DescribeAccessPoints(ctx context.Context, input *efs.DescribeAccessPointsInput, options ...func(*efs.Options)) (*efs.DescribeAccessPointsOutput, error) { + return &efs.DescribeAccessPointsOutput{ + AccessPoints: []efsTypes.AccessPointDescription{ + { + AccessPointId: aws.String("fsap-12345678"), + FileSystemId: aws.String("fs-12345678"), + Name: aws.String("fsap-12345678"), + PosixUser: &efsTypes.PosixUser{ + Gid: aws.Int64(1000), + Uid: aws.Int64(1000), + }, + }, + { + AccessPointId: aws.String("fsap-87654321"), + FileSystemId: aws.String("fs-87654321"), + Name: aws.String("fsap-12345679"), + PosixUser: &efsTypes.PosixUser{ + Gid: aws.Int64(1000), + Uid: aws.Int64(1000), + }, + }, + }, + }, nil +} diff --git a/aws/sdk/eks.go b/aws/sdk/eks.go index 885cf07..9f6f2b7 100644 --- a/aws/sdk/eks.go +++ b/aws/sdk/eks.go @@ -18,7 +18,7 @@ type EKSClientInterface interface { ListNodegroups(context.Context, *eks.ListNodegroupsInput, ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) } -func RegisterEKSTypes() { +func init() { gob.Register([]string{}) gob.Register(eksTypes.Cluster{}) gob.Register(eksTypes.Nodegroup{}) @@ -126,7 +126,7 @@ func CachedEKSListNodeGroups(client EKSClientInterface, accountID string, region func CachedEKSDescribeNodeGroup(client EKSClientInterface, accountID string, region string, clusterName string, nodeGroupName string) (eksTypes.Nodegroup, error) { var nodeGroup eksTypes.Nodegroup - cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, nodeGroupName) + cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, clusterName, nodeGroupName) cached, found := internal.Cache.Get(cacheKey) if found { return cached.(eksTypes.Nodegroup), nil diff --git a/aws/sdk/eks_mocks.go b/aws/sdk/eks_mocks.go new file mode 100644 index 0000000..28e961e --- /dev/null +++ b/aws/sdk/eks_mocks.go @@ -0,0 +1,54 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types" +) + +type MockedAWSEksClient struct { +} + +func (m *MockedAWSEksClient) ListClusters(ctx context.Context, input *eks.ListClustersInput, options ...func(*eks.Options)) (*eks.ListClustersOutput, error) { + return &eks.ListClustersOutput{ + Clusters: []string{ + "cluster1", + "cluster2", + }, + }, nil +} + +func (m *MockedAWSEksClient) DescribeCluster(ctx context.Context, input *eks.DescribeClusterInput, options ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) { + return &eks.DescribeClusterOutput{ + Cluster: &eksTypes.Cluster{ + Arn: aws.String("arn:aws:eks:us-east-1:123456789012:cluster/cluster1"), + Endpoint: aws.String("https://cluster1.us-east-1.eks.amazonaws.com"), + Name: aws.String("cluster1"), + PlatformVersion: aws.String("eks.1"), + Version: aws.String("1.18"), + RoleArn: aws.String("arn:aws:iam::123456789012:role/eks-role"), + }, + }, nil +} + +func (m *MockedAWSEksClient) ListNodegroups(ctx context.Context, input *eks.ListNodegroupsInput, options ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) { + return &eks.ListNodegroupsOutput{ + Nodegroups: []string{ + "nodegroup1", + "nodegroup2", + }, + }, nil +} + +func (m *MockedAWSEksClient) DescribeNodegroup(ctx context.Context, input *eks.DescribeNodegroupInput, options ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) { + return &eks.DescribeNodegroupOutput{ + Nodegroup: &eksTypes.Nodegroup{ + ClusterName: aws.String("cluster1"), + NodeRole: aws.String("arn:aws:iam::123456789012:role/eks-role"), + NodegroupName: aws.String("nodegroup1"), + NodegroupArn: aws.String("arn:aws:eks:us-east-1:123456789012:nodegroup/cluster1/nodegroup1"), + }, + }, nil +} diff --git a/aws/sdk/elasticache.go b/aws/sdk/elasticache.go new file mode 100644 index 0000000..c7f6b16 --- /dev/null +++ b/aws/sdk/elasticache.go @@ -0,0 +1,57 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/elasticache" + elasticacheTypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types" + "github.com/patrickmn/go-cache" +) + +type AWSElastiCacheClientInterface interface { + DescribeCacheClusters(context.Context, *elasticache.DescribeCacheClustersInput, ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) +} + +func init() { + gob.Register([]elasticacheTypes.CacheCluster{}) + +} + +func CachedElastiCacheDescribeCacheClusters(client AWSElastiCacheClientInterface, accountID string, region string) ([]elasticacheTypes.CacheCluster, error) { + var PaginationControl *string + var clusters []elasticacheTypes.CacheCluster + cacheKey := fmt.Sprintf("%s-elasticache-DescribeCacheClusters-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]elasticacheTypes.CacheCluster), nil + } + for { + DescribeCacheClusters, err := client.DescribeCacheClusters( + context.TODO(), + &elasticache.DescribeCacheClustersInput{ + Marker: PaginationControl, + }, + func(o *elasticache.Options) { + o.Region = region + }, + ) + + if err != nil { + return clusters, err + } + + clusters = append(clusters, DescribeCacheClusters.CacheClusters...) + + //pagination + if DescribeCacheClusters.Marker == nil { + break + } + PaginationControl = DescribeCacheClusters.Marker + } + + internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration) + return clusters, nil +} diff --git a/aws/sdk/elasticache_mocks.go b/aws/sdk/elasticache_mocks.go new file mode 100644 index 0000000..55f6907 --- /dev/null +++ b/aws/sdk/elasticache_mocks.go @@ -0,0 +1,40 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticache" + elasticacheTypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types" +) + +type MockedElasticacheClient struct { +} + +func (m *MockedElasticacheClient) DescribeCacheClusters(ctx context.Context, input *elasticache.DescribeCacheClustersInput, options ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) { + return &elasticache.DescribeCacheClustersOutput{ + CacheClusters: []elasticacheTypes.CacheCluster{ + { + CacheClusterId: aws.String("test"), + ARN: aws.String("arn:aws:elasticache:us-east-1:123456789012:cluster:myCluster"), + Engine: aws.String("redis"), + EngineVersion: aws.String("6.x"), + CacheNodeType: aws.String("cache.t3.micro"), + NumCacheNodes: aws.Int32(1), + CacheClusterStatus: aws.String("available"), + PreferredAvailabilityZone: aws.String("us-east-1a"), + CacheSubnetGroupName: aws.String("default"), + ReplicationGroupId: aws.String("test"), + SecurityGroups: []elasticacheTypes.SecurityGroupMembership{ + { + SecurityGroupId: aws.String("test"), + Status: aws.String("active"), + }, + }, + + AutoMinorVersionUpgrade: aws.Bool(true), + PreferredMaintenanceWindow: aws.String("sun:05:00-sun:06:00"), + }, + }, + }, nil +} diff --git a/aws/sdk/elasticbeanstalk.go b/aws/sdk/elasticbeanstalk.go new file mode 100644 index 0000000..f787a15 --- /dev/null +++ b/aws/sdk/elasticbeanstalk.go @@ -0,0 +1,35 @@ +package sdk + +import ( + "context" + "encoding/gob" + + "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk" + elasticbeanstalkTypes "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk/types" +) + +type AWSElasticBeanstalkClientInterface interface { + DescribeApplications(context.Context, *elasticbeanstalk.DescribeApplicationsInput, ...func(*elasticbeanstalk.Options)) (*elasticbeanstalk.DescribeApplicationsOutput, error) +} + +func init() { + gob.Register([]elasticbeanstalkTypes.ApplicationDescription{}) +} + +func CachedElasticBeanstalkDescribeApplications(client AWSElasticBeanstalkClientInterface, accountID string, region string) ([]elasticbeanstalkTypes.ApplicationDescription, error) { + var applications []elasticbeanstalkTypes.ApplicationDescription + DescribeApplications, err := client.DescribeApplications( + context.TODO(), + &elasticbeanstalk.DescribeApplicationsInput{}, + func(o *elasticbeanstalk.Options) { + o.Region = region + }, + ) + + if err != nil { + return applications, err + } + + applications = append(applications, DescribeApplications.Applications...) + return applications, nil +} diff --git a/aws/sdk/elasticbeanstalk_mocks.go b/aws/sdk/elasticbeanstalk_mocks.go new file mode 100644 index 0000000..62441f0 --- /dev/null +++ b/aws/sdk/elasticbeanstalk_mocks.go @@ -0,0 +1,33 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk" + elasticbeanstalkTypes "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk/types" +) + +type MockedElasticBeanstalkClient struct { +} + +func (m *MockedElasticBeanstalkClient) DescribeApplications(ctx context.Context, input *elasticbeanstalk.DescribeApplicationsInput, options ...func(*elasticbeanstalk.Options)) (*elasticbeanstalk.DescribeApplicationsOutput, error) { + return &elasticbeanstalk.DescribeApplicationsOutput{ + Applications: []elasticbeanstalkTypes.ApplicationDescription{ + { + ApplicationName: aws.String("app1"), + ApplicationArn: aws.String("arn:aws:elasticbeanstalk:us-east-1:123456789012:application/app1"), + ConfigurationTemplates: []string{ + "template1", + }, + }, + { + ApplicationName: aws.String("app2"), + ApplicationArn: aws.String("arn:aws:elasticbeanstalk:us-east-1:123456789012:application/app2"), + ConfigurationTemplates: []string{ + "template2", + }, + }, + }, + }, nil +} diff --git a/aws/sdk/elb.go b/aws/sdk/elb.go index a8faa2c..8bf2431 100644 --- a/aws/sdk/elb.go +++ b/aws/sdk/elb.go @@ -15,7 +15,7 @@ type ELBClientInterface interface { DescribeLoadBalancers(context.Context, *elasticloadbalancing.DescribeLoadBalancersInput, ...func(*elasticloadbalancing.Options)) (*elasticloadbalancing.DescribeLoadBalancersOutput, error) } -func RegisterELBTypes() { +func init() { gob.Register([]elbTypes.LoadBalancerDescription{}) } diff --git a/aws/sdk/elb_mocks.go b/aws/sdk/elb_mocks.go new file mode 100644 index 0000000..299f6d0 --- /dev/null +++ b/aws/sdk/elb_mocks.go @@ -0,0 +1,37 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" + elbTypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" +) + +type MockedElbClient struct { +} + +func (m *MockedElbClient) DescribeLoadBalancers(ctx context.Context, input *elasticloadbalancing.DescribeLoadBalancersInput, options ...func(*elasticloadbalancing.Options)) (*elasticloadbalancing.DescribeLoadBalancersOutput, error) { + return &elasticloadbalancing.DescribeLoadBalancersOutput{ + LoadBalancerDescriptions: []elbTypes.LoadBalancerDescription{ + { + LoadBalancerName: aws.String("elb1"), + DNSName: aws.String("elb1"), + Instances: []elbTypes.Instance{ + { + InstanceId: aws.String("i-1234567890abcdef0"), + }, + }, + }, + { + LoadBalancerName: aws.String("elb2"), + DNSName: aws.String("elb2"), + Instances: []elbTypes.Instance{ + { + InstanceId: aws.String("i-1234567890abcdef1"), + }, + }, + }, + }, + }, nil +} diff --git a/aws/sdk/elbv2.go b/aws/sdk/elbv2.go index 9161435..43d9c3b 100644 --- a/aws/sdk/elbv2.go +++ b/aws/sdk/elbv2.go @@ -15,7 +15,7 @@ type ELBv2ClientInterface interface { DescribeLoadBalancers(context.Context, *elasticloadbalancingv2.DescribeLoadBalancersInput, ...func(*elasticloadbalancingv2.Options)) (*elasticloadbalancingv2.DescribeLoadBalancersOutput, error) } -func RegisterELBv2Types() { +func init() { gob.Register([]elbV2Types.LoadBalancer{}) } diff --git a/aws/sdk/elbv2_mocks.go b/aws/sdk/elbv2_mocks.go new file mode 100644 index 0000000..f447e45 --- /dev/null +++ b/aws/sdk/elbv2_mocks.go @@ -0,0 +1,58 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" + elbv2Types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" +) + +type MockedElbv2Client struct { +} + +func (m *MockedElbv2Client) DescribeLoadBalancers(ctx context.Context, input *elasticloadbalancingv2.DescribeLoadBalancersInput, options ...func(*elasticloadbalancingv2.Options)) (*elasticloadbalancingv2.DescribeLoadBalancersOutput, error) { + return &elasticloadbalancingv2.DescribeLoadBalancersOutput{ + LoadBalancers: []elbv2Types.LoadBalancer{ + { + LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188"), + DNSName: aws.String("my-load-balancer-424835706.us-east-1.elb.amazonaws.com"), + CanonicalHostedZoneId: aws.String("Z2P70J7EXAMPLE"), + CreatedTime: aws.Time(time.Now()), + LoadBalancerName: aws.String("my-load-balancer"), + Scheme: elbv2Types.LoadBalancerSchemeEnumInternetFacing, + VpcId: aws.String("vpc-3ac0fb5f"), + State: &elbv2Types.LoadBalancerState{ + Code: elbv2Types.LoadBalancerStateEnumActive, + Reason: aws.String(""), + }, + Type: elbv2Types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2Types.AvailabilityZone{ + { + ZoneName: aws.String("us-east-1a"), + SubnetId: aws.String("subnet-8360a9e7"), + LoadBalancerAddresses: []elbv2Types.LoadBalancerAddress{ + { + IpAddress: aws.String("1.2.3.4"), + }, + }, + }, + { + ZoneName: aws.String("us-east-1b"), + SubnetId: aws.String("subnet-b7d581c0"), + LoadBalancerAddresses: []elbv2Types.LoadBalancerAddress{ + { + IpAddress: aws.String("2.3.4.5"), + }, + }, + }, + }, + SecurityGroups: []string{ + "sg-5943793c", + }, + IpAddressType: elbv2Types.IpAddressTypeDualstack, + }, + }, + }, nil +} diff --git a/aws/sdk/emr.go b/aws/sdk/emr.go new file mode 100644 index 0000000..9c16ff4 --- /dev/null +++ b/aws/sdk/emr.go @@ -0,0 +1,98 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/emr" + emrTypes "github.com/aws/aws-sdk-go-v2/service/emr/types" + "github.com/patrickmn/go-cache" +) + +type AWSEMRClientInterface interface { + ListClusters(context.Context, *emr.ListClustersInput, ...func(*emr.Options)) (*emr.ListClustersOutput, error) + ListInstances(context.Context, *emr.ListInstancesInput, ...func(*emr.Options)) (*emr.ListInstancesOutput, error) +} + +func init() { + gob.Register([]emrTypes.ClusterSummary{}) + + //need to do this to avoid conflicts with the Instance type in the ec2 package + type EMRInstance emrTypes.Instance + gob.Register([]EMRInstance{}) +} + +func CachedEMRListClusters(client AWSEMRClientInterface, accountID string, region string) ([]emrTypes.ClusterSummary, error) { + var PaginationControl *string + var clusters []emrTypes.ClusterSummary + cacheKey := fmt.Sprintf("%s-emr-ListClusters-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]emrTypes.ClusterSummary), nil + } + for { + ListClusters, err := client.ListClusters( + context.TODO(), + &emr.ListClustersInput{ + Marker: PaginationControl, + }, + func(o *emr.Options) { + o.Region = region + }, + ) + + if err != nil { + return clusters, err + } + + clusters = append(clusters, ListClusters.Clusters...) + + //pagination + if ListClusters.Marker == nil { + break + } + PaginationControl = ListClusters.Marker + } + + internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration) + return clusters, nil +} + +func CachedEMRListInstances(client AWSEMRClientInterface, accountID string, region string, clusterID string) ([]emrTypes.Instance, error) { + var PaginationControl *string + var instances []emrTypes.Instance + cacheKey := fmt.Sprintf("%s-emr-ListInstances-%s-%s", accountID, region, clusterID) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]emrTypes.Instance), nil + } + for { + ListInstances, err := client.ListInstances( + context.TODO(), + &emr.ListInstancesInput{ + ClusterId: &clusterID, + Marker: PaginationControl, + }, + func(o *emr.Options) { + o.Region = region + }, + ) + + if err != nil { + return instances, err + } + + instances = append(instances, ListInstances.Instances...) + + //pagination + if ListInstances.Marker == nil { + break + } + PaginationControl = ListInstances.Marker + } + + internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration) + return instances, nil +} diff --git a/aws/sdk/emr_mocks.go b/aws/sdk/emr_mocks.go new file mode 100644 index 0000000..8d2e9a3 --- /dev/null +++ b/aws/sdk/emr_mocks.go @@ -0,0 +1,44 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/emr" + emrTypes "github.com/aws/aws-sdk-go-v2/service/emr/types" +) + +type MockedEMRClient struct { +} + +func (m *MockedEMRClient) ListClusters(ctx context.Context, input *emr.ListClustersInput, options ...func(*emr.Options)) (*emr.ListClustersOutput, error) { + return &emr.ListClustersOutput{ + Clusters: []emrTypes.ClusterSummary{ + { + Id: aws.String("cluster1"), + }, + { + Id: aws.String("cluster2"), + }, + }, + }, nil +} + +func (m *MockedEMRClient) ListInstances(ctx context.Context, input *emr.ListInstancesInput, options ...func(*emr.Options)) (*emr.ListInstancesOutput, error) { + return &emr.ListInstancesOutput{ + Instances: []emrTypes.Instance{ + { + Id: aws.String("instance1"), + InstanceType: aws.String("m5.xlarge"), + Ec2InstanceId: aws.String("i-1234567890"), + PrivateDnsName: aws.String("ip-10-0-0-1.ec2.internal"), + PublicDnsName: aws.String("ec2-1-2-3-4.compute-1.amazonaws.com"), + PrivateIpAddress: aws.String("10.0.0.1"), + PublicIpAddress: aws.String("1.2.3.4"), + }, + { + Id: aws.String("instance2"), + }, + }, + }, nil +} diff --git a/aws/sdk/glue.go b/aws/sdk/glue.go new file mode 100644 index 0000000..aca4c44 --- /dev/null +++ b/aws/sdk/glue.go @@ -0,0 +1,247 @@ +package sdk + +import ( + "context" + "encoding/gob" + "errors" + + "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/glue" + glueTypes "github.com/aws/aws-sdk-go-v2/service/glue/types" + "github.com/patrickmn/go-cache" +) + +type AWSGlueClientInterface interface { + ListDevEndpoints(ctx context.Context, params *glue.ListDevEndpointsInput, optFns ...func(*glue.Options)) (*glue.ListDevEndpointsOutput, error) + ListJobs(ctx context.Context, params *glue.ListJobsInput, optFns ...func(*glue.Options)) (*glue.ListJobsOutput, error) + GetTables(ctx context.Context, params *glue.GetTablesInput, optFns ...func(*glue.Options)) (*glue.GetTablesOutput, error) + GetDatabases(ctx context.Context, params *glue.GetDatabasesInput, optFns ...func(*glue.Options)) (*glue.GetDatabasesOutput, error) + GetResourcePolicies(ctx context.Context, params *glue.GetResourcePoliciesInput, optFns ...func(*glue.Options)) (*glue.GetResourcePoliciesOutput, error) +} + +func init() { + gob.Register([]string{}) + gob.Register(glueTypes.DevEndpoint{}) + gob.Register(glueTypes.Job{}) + gob.Register([]glueTypes.Table{}) + gob.Register([]glueTypes.Database{}) +} + +func CachedGlueListDevEndpoints(GlueClient AWSGlueClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var devEndpoints []string + cacheKey := "glue-ListDevEndpoints-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached Glue dev endpoints data") + return cached.([]string), nil + } + + for { + ListDevEndpoints, err := GlueClient.ListDevEndpoints( + context.TODO(), + &glue.ListDevEndpointsInput{ + NextToken: PaginationControl, + }, + func(o *glue.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + devEndpoints = append(devEndpoints, ListDevEndpoints.DevEndpointNames...) + + // Pagination control. + if aws.ToString(ListDevEndpoints.NextToken) != "" { + PaginationControl = ListDevEndpoints.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, devEndpoints, cache.DefaultExpiration) + + return devEndpoints, nil +} + +func CachedGlueListJobs(GlueClient AWSGlueClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var jobs []string + cacheKey := "glue-ListJobs-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached Glue jobs data") + return cached.([]string), nil + } + + for { + ListJobs, err := GlueClient.ListJobs( + context.TODO(), + &glue.ListJobsInput{ + NextToken: PaginationControl, + }, + func(o *glue.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + jobs = append(jobs, ListJobs.JobNames...) + + // Pagination control. + if ListJobs.NextToken != nil { + PaginationControl = ListJobs.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, jobs, cache.DefaultExpiration) + + return jobs, nil +} + +func CachedGlueGetTables(GlueClient AWSGlueClientInterface, accountID string, region string, dbName string) ([]glueTypes.Table, error) { + var PaginationControl *string + var tables []glueTypes.Table + cacheKey := "glue-GetTables-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached Glue tables data") + return cached.([]glueTypes.Table), nil + } + + for { + GetTables, err := GlueClient.GetTables( + context.TODO(), + &glue.GetTablesInput{ + DatabaseName: &dbName, + NextToken: PaginationControl, + }, + func(o *glue.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + tables = append(tables, GetTables.TableList...) + + // Pagination control. + if GetTables.NextToken != nil { + PaginationControl = GetTables.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, tables, cache.DefaultExpiration) + + return tables, nil +} + +func CachedGlueGetDatabases(GlueClient AWSGlueClientInterface, accountID string, region string) ([]glueTypes.Database, error) { + var PaginationControl *string + var databases []glueTypes.Database + cacheKey := "glue-GetDatabases-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached Glue databases data") + return cached.([]glueTypes.Database), nil + } + + for { + GetDatabases, err := GlueClient.GetDatabases( + context.TODO(), + &glue.GetDatabasesInput{ + NextToken: PaginationControl, + }, + func(o *glue.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + databases = append(databases, GetDatabases.DatabaseList...) + + // Pagination control. + if GetDatabases.NextToken != nil { + PaginationControl = GetDatabases.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, databases, cache.DefaultExpiration) + + return databases, nil +} + +func CachedGlueGetResourcePolicies(GlueClient AWSGlueClientInterface, accountID string, region string) ([]policy.Policy, error) { + var PaginationControl *string + var GluePolicy policy.Policy + var policies []policy.Policy + var policyJSON string + cacheKey := "glue-GetResourcePolicies-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached Glue resource policies data") + return cached.([]policy.Policy), nil + } + + for { + GetResourcePolicies, err := GlueClient.GetResourcePolicies( + context.TODO(), + &glue.GetResourcePoliciesInput{ + NextToken: PaginationControl, + }, + func(o *glue.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + for _, policyPointer := range GetResourcePolicies.GetResourcePoliciesResponseList { + policyJSON = aws.ToString(policyPointer.PolicyInJson) + GluePolicy, err = policy.ParseJSONPolicy([]byte(policyJSON)) + if err != nil { + return policies, errors.New("error parsing Glue policy") + } + policies = append(policies, GluePolicy) + } + + // Pagination control. + if GetResourcePolicies.NextToken != nil { + PaginationControl = GetResourcePolicies.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, policies, cache.DefaultExpiration) + + return policies, nil +} + +// in the resource trust command, need to parse the actual resource policies to determine the resources for the cloudfox table. crazy pants diff --git a/aws/sdk/glue_mocks.go b/aws/sdk/glue_mocks.go new file mode 100644 index 0000000..d70bcd4 --- /dev/null +++ b/aws/sdk/glue_mocks.go @@ -0,0 +1,80 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/glue" + glueTypes "github.com/aws/aws-sdk-go-v2/service/glue/types" +) + +type MockedGlueClient struct { +} + +func (m *MockedGlueClient) ListDevEndpoints(ctx context.Context, input *glue.ListDevEndpointsInput, options ...func(*glue.Options)) (*glue.ListDevEndpointsOutput, error) { + return &glue.ListDevEndpointsOutput{ + DevEndpointNames: []string{ + "devendpoint1", + "devendpoint2", + }, + }, nil +} + +func (m *MockedGlueClient) ListJobs(ctx context.Context, input *glue.ListJobsInput, options ...func(*glue.Options)) (*glue.ListJobsOutput, error) { + return &glue.ListJobsOutput{ + JobNames: []string{ + "job1", + "job2", + }, + }, nil +} + +func (m *MockedGlueClient) GetTables(ctx context.Context, input *glue.GetTablesInput, options ...func(*glue.Options)) (*glue.GetTablesOutput, error) { + return &glue.GetTablesOutput{ + TableList: []glueTypes.Table{ + { + Name: aws.String("table1"), + DatabaseName: aws.String("database1"), + Description: aws.String("description1"), + Parameters: map[string]string{ + "param1": "value1", + "param2": "value2", + }, + }, + { + Name: aws.String("table2"), + DatabaseName: aws.String("database2"), + Description: aws.String("description2"), + Parameters: map[string]string{ + "param1": "value1", + "param2": "value2", + }, + }, + }, + }, nil +} + +func (m *MockedGlueClient) GetDatabases(ctx context.Context, input *glue.GetDatabasesInput, options ...func(*glue.Options)) (*glue.GetDatabasesOutput, error) { + return &glue.GetDatabasesOutput{ + DatabaseList: []glueTypes.Database{ + { + Name: aws.String("database1"), + Description: aws.String("description1"), + LocationUri: aws.String("s3://bucket1"), + Parameters: map[string]string{ + "param1": "value1", + "param2": "value2", + }, + }, + { + Name: aws.String("database2"), + Description: aws.String("description2"), + LocationUri: aws.String("s3://bucket2"), + Parameters: map[string]string{ + "param1": "value1", + "param2": "value2", + }, + }, + }, + }, nil +} diff --git a/aws/sdk/grafana.go b/aws/sdk/grafana.go index 8e1f260..35c40f4 100644 --- a/aws/sdk/grafana.go +++ b/aws/sdk/grafana.go @@ -15,7 +15,7 @@ type GrafanaClientInterface interface { ListWorkspaces(context.Context, *grafana.ListWorkspacesInput, ...func(*grafana.Options)) (*grafana.ListWorkspacesOutput, error) } -func RegisterGrafanaTypes() { +func init() { gob.Register([]grafanaTypes.WorkspaceSummary{}) } diff --git a/aws/sdk/grafana_mocks.go b/aws/sdk/grafana_mocks.go new file mode 100644 index 0000000..96a986e --- /dev/null +++ b/aws/sdk/grafana_mocks.go @@ -0,0 +1,42 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/grafana" + grafanaTypes "github.com/aws/aws-sdk-go-v2/service/grafana/types" +) + +type MockedGrafanaClient struct { +} + +func (m *MockedGrafanaClient) ListWorkspaces(ctx context.Context, input *grafana.ListWorkspacesInput, options ...func(*grafana.Options)) (*grafana.ListWorkspacesOutput, error) { + return &grafana.ListWorkspacesOutput{ + Workspaces: []grafanaTypes.WorkspaceSummary{ + { + Authentication: &grafanaTypes.AuthenticationSummary{ + Providers: []grafanaTypes.AuthenticationProviderTypes{ + grafanaTypes.AuthenticationProviderTypesAwsSso, + }, + }, + Created: aws.Time(time.Now()), + Id: aws.String("workspace1"), + Name: aws.String("workspace1"), + Status: grafanaTypes.WorkspaceStatusActive, + }, + { + Authentication: &grafanaTypes.AuthenticationSummary{ + Providers: []grafanaTypes.AuthenticationProviderTypes{ + grafanaTypes.AuthenticationProviderTypesAwsSso, + }, + }, + Created: aws.Time(time.Now()), + Id: aws.String("workspace2"), + Name: aws.String("workspace2"), + Status: grafanaTypes.WorkspaceStatusActive, + }, + }, + }, nil +} diff --git a/aws/sdk/iam.go b/aws/sdk/iam.go index 37e8581..9ad0e30 100644 --- a/aws/sdk/iam.go +++ b/aws/sdk/iam.go @@ -22,12 +22,14 @@ type AWSIAMClientInterface interface { GetAccountAuthorizationDetails(ctx context.Context, params *iam.GetAccountAuthorizationDetailsInput, optFns ...func(*iam.Options)) (*iam.GetAccountAuthorizationDetailsOutput, error) SimulatePrincipalPolicy(ctx context.Context, params *iam.SimulatePrincipalPolicyInput, optFns ...func(*iam.Options)) (*iam.SimulatePrincipalPolicyOutput, error) ListInstanceProfiles(ctx context.Context, params *iam.ListInstanceProfilesInput, optFns ...func(*iam.Options)) (*iam.ListInstanceProfilesOutput, error) + ListGroups(ctx context.Context, params *iam.ListGroupsInput, optFns ...func(*iam.Options)) (*iam.ListGroupsOutput, error) } -func RegisterIamTypes() { +func init() { gob.Register([]iamTypes.User{}) gob.Register([]iamTypes.AccessKeyMetadata{}) gob.Register([]iamTypes.Role{}) + gob.Register([]iamTypes.Group{}) gob.Register([]iamTypes.PolicyDetail{}) gob.Register([]iamTypes.InstanceProfile{}) gob.Register([]iamTypes.EvaluationResult{}) @@ -205,7 +207,7 @@ func CachedIamSimulatePrincipalPolicy(IAMClient AWSIAMClientInterface, accountID var EvaluationResults []iamTypes.EvaluationResult md5hashedActionNames := md5.Sum([]byte(strings.Join(actionNames, ""))) md5hashedResourceArns := md5.Sum([]byte(strings.Join(resourceArns, ""))) - // proccess arn and get the name of the resource + // process arn and get the name of the resource arn, err := arn.Parse(*principal) if err != nil { return EvaluationResults, err @@ -250,3 +252,39 @@ func CachedIamSimulatePrincipalPolicy(IAMClient AWSIAMClientInterface, accountID internal.Cache.Set(cacheKey, EvaluationResults, cache.DefaultExpiration) return EvaluationResults, nil } + +func CachedIamListGroups(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.Group, error) { + var PaginationControl *string + var Groups []iamTypes.Group + cacheKey := fmt.Sprintf("%s-iam-ListGroups", accountID) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]iamTypes.Group), nil + } + + for { + ListGroups, err := IAMClient.ListGroups( + context.TODO(), + &iam.ListGroupsInput{ + Marker: PaginationControl, + }, + ) + if err != nil { + return Groups, err + } + + Groups = append(Groups, ListGroups.Groups...) + + // Pagination control. + if ListGroups.Marker != nil { + PaginationControl = ListGroups.Marker + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, Groups, cache.DefaultExpiration) + return Groups, nil + +} diff --git a/aws/sdk/iam_mocks.go b/aws/sdk/iam_mocks.go new file mode 100644 index 0000000..a4d1196 --- /dev/null +++ b/aws/sdk/iam_mocks.go @@ -0,0 +1,222 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" +) + +type MockedIAMClient struct { +} + +func (m *MockedIAMClient) ListUsers(ctx context.Context, input *iam.ListUsersInput, options ...func(*iam.Options)) (*iam.ListUsersOutput, error) { + return &iam.ListUsersOutput{ + Users: []iamTypes.User{ + { + Arn: aws.String("arn:aws:iam::123456789012:user/user1"), + CreateDate: aws.Time(time.Now()), + Path: aws.String("/"), + UserId: aws.String("123456789012"), + UserName: aws.String("user1"), + }, + { + Arn: aws.String("arn:aws:iam::123456789012:user/user2"), + CreateDate: aws.Time(time.Now()), + Path: aws.String("/"), + UserId: aws.String("123456789012"), + UserName: aws.String("user2"), + }, + }, + }, nil +} + +func (m *MockedIAMClient) ListAccessKeys(ctx context.Context, input *iam.ListAccessKeysInput, options ...func(*iam.Options)) (*iam.ListAccessKeysOutput, error) { + return &iam.ListAccessKeysOutput{ + AccessKeyMetadata: []iamTypes.AccessKeyMetadata{ + { + AccessKeyId: aws.String("accesskey1"), + CreateDate: aws.Time(time.Now()), + Status: iamTypes.StatusTypeActive, + UserName: aws.String("user1"), + }, + { + AccessKeyId: aws.String("accesskey2"), + CreateDate: aws.Time(time.Now()), + Status: iamTypes.StatusTypeActive, + UserName: aws.String("user2"), + }, + }, + }, nil + +} + +func (m *MockedIAMClient) ListGroups(ctx context.Context, input *iam.ListGroupsInput, options ...func(*iam.Options)) (*iam.ListGroupsOutput, error) { + return &iam.ListGroupsOutput{ + Groups: []iamTypes.Group{ + { + Arn: aws.String("arn:aws:iam::123456789012:group/group1"), + CreateDate: aws.Time(time.Now()), + GroupId: aws.String("123456789012"), + GroupName: aws.String("group1"), + Path: aws.String("/"), + }, + { + Arn: aws.String("arn:aws:iam::123456789012:group/group2"), + CreateDate: aws.Time(time.Now()), + GroupId: aws.String("123456789012"), + GroupName: aws.String("group2"), + Path: aws.String("/"), + }, + }, + }, nil + +} + +func (m *MockedIAMClient) ListRoles(ctx context.Context, input *iam.ListRolesInput, options ...func(*iam.Options)) (*iam.ListRolesOutput, error) { + return &iam.ListRolesOutput{ + Roles: []iamTypes.Role{ + { + Arn: aws.String("arn:aws:iam::123456789012:role/role1"), + AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"), + CreateDate: aws.Time(time.Now()), + RoleId: aws.String("123456789012"), + RoleName: aws.String("role1"), + Path: aws.String("/"), + }, + { + Arn: aws.String("arn:aws:iam::123456789012:role/role2"), + AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"), + CreateDate: aws.Time(time.Now()), + RoleId: aws.String("123456789012"), + RoleName: aws.String("role2"), + Path: aws.String("/"), + }, + { + Arn: aws.String("arn:aws:iam::123456789012:role/role3"), + AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::123456789012:root\"},\"Action\":\"sts:AssumeRole\"}]}"), + CreateDate: aws.Time(time.Now()), + RoleId: aws.String("123456789012"), + RoleName: aws.String("role3"), + Path: aws.String("/"), + }, + }, + }, nil + +} + +func (m *MockedIAMClient) GetAccountAuthorizationDetails(ctx context.Context, params *iam.GetAccountAuthorizationDetailsInput, optFns ...func(*iam.Options)) (*iam.GetAccountAuthorizationDetailsOutput, error) { + return &iam.GetAccountAuthorizationDetailsOutput{ + GroupDetailList: []iamTypes.GroupDetail{ + { + Arn: aws.String("arn:aws:iam::123456789012:group/group1"), + CreateDate: aws.Time(time.Now()), + GroupId: aws.String("123456789012"), + GroupName: aws.String("group1"), + Path: aws.String("/"), + }, + { + Arn: aws.String("arn:aws:iam::123456789012:group/group2"), + CreateDate: aws.Time(time.Now()), + GroupId: aws.String("123456789012"), + GroupName: aws.String("group2"), + Path: aws.String("/"), + }, + }, + RoleDetailList: []iamTypes.RoleDetail{ + { + Arn: aws.String("arn:aws:iam::123456789012:role/role1"), + AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"), + CreateDate: aws.Time(time.Now()), + RoleId: aws.String("123456789012"), + RoleName: aws.String("role1"), + Path: aws.String("/"), + }, + { + Arn: aws.String("arn:aws:iam::123456789012:role/role2"), + AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"), + CreateDate: aws.Time(time.Now()), + RoleId: aws.String("123456789012"), + RoleName: aws.String("role2"), + Path: aws.String("/"), + }, + { + Arn: aws.String("arn:aws:iam::123456789012:role/role3"), + AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::123456789012:root\"},\"Action\":\"sts:AssumeRole\"}]}"), + CreateDate: aws.Time(time.Now()), + RoleId: aws.String("123456789012"), + + RoleName: aws.String("role3"), + Path: aws.String("/"), + }, + }, + UserDetailList: []iamTypes.UserDetail{ + { + Arn: aws.String("arn:aws:iam::123456789012:user/user1"), + CreateDate: aws.Time(time.Now()), + + UserId: aws.String("123456789012"), + UserName: aws.String("user1"), + Path: aws.String("/"), + }, + { + Arn: aws.String("arn:aws:iam::123456789012:user/user2"), + CreateDate: aws.Time(time.Now()), + + UserId: aws.String("123456789012"), + UserName: aws.String("user2"), + Path: aws.String("/"), + }, + }, + }, nil +} + +func (m *MockedIAMClient) SimulatePrincipalPolicy(ctx context.Context, params *iam.SimulatePrincipalPolicyInput, optFns ...func(*iam.Options)) (*iam.SimulatePrincipalPolicyOutput, error) { + return &iam.SimulatePrincipalPolicyOutput{ + EvaluationResults: []iamTypes.EvaluationResult{ + { + EvalActionName: aws.String("sts:AssumeRole"), + EvalDecision: iamTypes.PolicyEvaluationDecisionTypeAllowed, + EvalResourceName: aws.String("arn:aws:iam::123456789012:role/role1"), + MatchedStatements: []iamTypes.Statement{ + { + SourcePolicyId: aws.String("PolicyForRole1"), + SourcePolicyType: iamTypes.PolicySourceTypeUser, + StartPosition: &iamTypes.Position{ + Column: 0, + Line: 0, + }, + }, + }, + }, + }, + }, nil + +} + +func (m *MockedIAMClient) ListInstanceProfiles(ctx context.Context, params *iam.ListInstanceProfilesInput, optFns ...func(*iam.Options)) (*iam.ListInstanceProfilesOutput, error) { + return &iam.ListInstanceProfilesOutput{ + InstanceProfiles: []iamTypes.InstanceProfile{ + { + Arn: aws.String("arn:aws:iam::123456789012:instance-profile/instance-profile1"), + CreateDate: aws.Time(time.Now()), + InstanceProfileId: aws.String("123456789012"), + InstanceProfileName: aws.String("instance-profile1"), + Path: aws.String("/"), + Roles: []iamTypes.Role{ + { + Arn: aws.String("arn:aws:iam::123456789012:role/role1"), + AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"), + CreateDate: aws.Time(time.Now()), + RoleId: aws.String("123456789012"), + RoleName: aws.String("role1"), + Path: aws.String("/"), + }, + }, + }, + }, + }, nil + +} diff --git a/aws/sdk/kinesis.go b/aws/sdk/kinesis.go new file mode 100644 index 0000000..1d13f12 --- /dev/null +++ b/aws/sdk/kinesis.go @@ -0,0 +1,55 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/kinesis" + "github.com/patrickmn/go-cache" +) + +type AWSKinesisClientInterface interface { + ListStreams(context.Context, *kinesis.ListStreamsInput, ...func(*kinesis.Options)) (*kinesis.ListStreamsOutput, error) +} + +func init() { + gob.Register([]string{}) +} + +func CachedKinesisListStreams(client AWSKinesisClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var streams []string + cacheKey := fmt.Sprintf("%s-kinesis-ListStreams-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]string), nil + } + for { + ListStreams, err := client.ListStreams( + context.TODO(), + &kinesis.ListStreamsInput{ + NextToken: PaginationControl, + }, + func(o *kinesis.Options) { + o.Region = region + }, + ) + + if err != nil { + return streams, err + } + + streams = append(streams, ListStreams.StreamNames...) + + //pagination + if ListStreams.NextToken == nil { + break + } + PaginationControl = ListStreams.NextToken + } + + internal.Cache.Set(cacheKey, streams, cache.DefaultExpiration) + return streams, nil +} diff --git a/aws/sdk/kinesis_mocks.go b/aws/sdk/kinesis_mocks.go new file mode 100644 index 0000000..a217b0c --- /dev/null +++ b/aws/sdk/kinesis_mocks.go @@ -0,0 +1,21 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/kinesis" +) + +type MockedKinesisClient struct { +} + +func (m *MockedKinesisClient) ListStreams(ctx context.Context, input *kinesis.ListStreamsInput, options ...func(*kinesis.Options)) (*kinesis.ListStreamsOutput, error) { + return &kinesis.ListStreamsOutput{ + HasMoreStreams: aws.Bool(false), + StreamNames: []string{ + "stream1", + "stream2", + }, + }, nil +} diff --git a/aws/sdk/lambda.go b/aws/sdk/lambda.go index 6fb2d07..a55fdd9 100644 --- a/aws/sdk/lambda.go +++ b/aws/sdk/lambda.go @@ -17,7 +17,7 @@ type LambdaClientInterface interface { GetFunctionUrlConfig(context.Context, *lambda.GetFunctionUrlConfigInput, ...func(*lambda.Options)) (*lambda.GetFunctionUrlConfigOutput, error) } -func RegisterLambdaTypes() { +func init() { gob.Register([]lambdaTypes.FunctionConfiguration{}) gob.Register(customGetFuntionURLOutput{}) } diff --git a/aws/sdk/lambda_mocks.go b/aws/sdk/lambda_mocks.go new file mode 100644 index 0000000..ee5cfca --- /dev/null +++ b/aws/sdk/lambda_mocks.go @@ -0,0 +1,62 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/lambda" + lambdaTypes "github.com/aws/aws-sdk-go-v2/service/lambda/types" +) + +type MockedLambdaClient struct { +} + +func (m *MockedLambdaClient) ListFunctions(ctx context.Context, input *lambda.ListFunctionsInput, options ...func(*lambda.Options)) (*lambda.ListFunctionsOutput, error) { + return &lambda.ListFunctionsOutput{ + Functions: []lambdaTypes.FunctionConfiguration{ + { + FunctionArn: aws.String("arn:aws:lambda:us-east-1:123456789012:function:my-function"), + FunctionName: aws.String("my-function"), + Handler: aws.String("index.handler"), + Runtime: lambdaTypes.RuntimeNodejs18x, + Environment: &lambdaTypes.EnvironmentResponse{ + Variables: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + { + FunctionArn: aws.String("arn:aws:lambda:us-east-1:123456789012:function:my-function2"), + FunctionName: aws.String("my-function2"), + Handler: aws.String("index.handler"), + Runtime: lambdaTypes.RuntimeNodejs18x, + }, + }, + }, nil +} + +func (m *MockedLambdaClient) GetFunction(ctx context.Context, input *lambda.GetFunctionInput, options ...func(*lambda.Options)) (*lambda.GetFunctionOutput, error) { + return &lambda.GetFunctionOutput{ + Configuration: &lambdaTypes.FunctionConfiguration{ + FunctionArn: aws.String("arn:aws:lambda:us-east-1:123456789012:function:my-function"), + FunctionName: aws.String("my-function"), + Handler: aws.String("index.handler"), + Runtime: lambdaTypes.RuntimeNodejs18x, + Environment: &lambdaTypes.EnvironmentResponse{ + Variables: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + }, nil +} + +func (m *MockedLambdaClient) GetFunctionUrlConfig(ctx context.Context, input *lambda.GetFunctionInput, options ...func(*lambda.Options)) (*lambda.GetFunctionUrlConfigOutput, error) { + return &lambda.GetFunctionUrlConfigOutput{ + FunctionUrl: aws.String("https://my-function.us-east-1.amazonaws.com/Prod/"), + FunctionArn: aws.String("arn:aws:lambda:us-east-1:123456789012:function:my-function"), + AuthType: lambdaTypes.FunctionUrlAuthTypeNone, + }, nil +} diff --git a/aws/sdk/lightsail.go b/aws/sdk/lightsail.go index 9c56e78..7f7849a 100644 --- a/aws/sdk/lightsail.go +++ b/aws/sdk/lightsail.go @@ -16,8 +16,12 @@ type lightsailClientInterface interface { GetContainerServices(context.Context, *lightsail.GetContainerServicesInput, ...func(*lightsail.Options)) (*lightsail.GetContainerServicesOutput, error) } -func RegisterLightsailTypes() { - //gob.Register([]lightsailTypes.Instance{}) +func init() { + + //need to do this to avoid conflicts with the Instance type in the ec2 package + type lightsailInstance lightsailTypes.Instance + gob.Register([]lightsailInstance{}) + gob.Register([]lightsailTypes.ContainerService{}) } diff --git a/aws/sdk/lightsail_mocks.go b/aws/sdk/lightsail_mocks.go new file mode 100644 index 0000000..a13eb87 --- /dev/null +++ b/aws/sdk/lightsail_mocks.go @@ -0,0 +1,80 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/lightsail" + lightsailTypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types" +) + +type MockedLightsailClient struct { +} + +func (m *MockedLightsailClient) GetInstances(ctx context.Context, input *lightsail.GetInstancesInput, options ...func(*lightsail.Options)) (*lightsail.GetInstancesOutput, error) { + return &lightsail.GetInstancesOutput{ + Instances: []lightsailTypes.Instance{ + { + BlueprintId: aws.String("blueprint1"), + BundleId: aws.String("bundle1"), + CreatedAt: aws.Time(time.Now()), + Location: &lightsailTypes.ResourceLocation{ + AvailabilityZone: aws.String("us-east-1a"), + RegionName: lightsailTypes.RegionNameUsEast1, + }, + Name: aws.String("instance1"), + Networking: &lightsailTypes.InstanceNetworking{ + MonthlyTransfer: &lightsailTypes.MonthlyTransfer{ + GbPerMonthAllocated: aws.Int32(1), + }, + }, + PrivateIpAddress: aws.String("10.1.1.1"), + PublicIpAddress: aws.String("1.2.3.4"), + }, + { + BlueprintId: aws.String("blueprint2"), + BundleId: aws.String("bundle2"), + CreatedAt: aws.Time(time.Now()), + Location: &lightsailTypes.ResourceLocation{ + AvailabilityZone: aws.String("us-east-1b"), + RegionName: lightsailTypes.RegionNameUsEast1, + }, + Name: aws.String("instance2"), + Networking: &lightsailTypes.InstanceNetworking{ + MonthlyTransfer: &lightsailTypes.MonthlyTransfer{ + GbPerMonthAllocated: aws.Int32(2), + }, + }, + PrivateIpAddress: aws.String("10.2.2.2"), + PublicIpAddress: aws.String("2.3.4.4"), + }, + }, + }, nil + +} + +func (m *MockedLightsailClient) GetContainerServices(ctx context.Context, input *lightsail.GetContainerServicesInput, options ...func(*lightsail.Options)) (*lightsail.GetContainerServicesOutput, error) { + return &lightsail.GetContainerServicesOutput{ + ContainerServices: []lightsailTypes.ContainerService{ + { + Arn: aws.String("arn1"), + Location: &lightsailTypes.ResourceLocation{ + AvailabilityZone: aws.String("us-east-1a"), + RegionName: lightsailTypes.RegionNameUsEast1, + }, + Url: aws.String("https://container1"), + PrivateDomainName: aws.String("container1"), + }, + { + Arn: aws.String("arn2"), + Location: &lightsailTypes.ResourceLocation{ + AvailabilityZone: aws.String("us-east-1a"), + RegionName: lightsailTypes.RegionNameUsEast1, + }, + Url: aws.String("https://container2"), + PrivateDomainName: aws.String("container2"), + }, + }, + }, nil +} diff --git a/aws/sdk/mq.go b/aws/sdk/mq.go index 2f6bd92..92cd055 100644 --- a/aws/sdk/mq.go +++ b/aws/sdk/mq.go @@ -15,7 +15,7 @@ type MQClientInterface interface { ListBrokers(context.Context, *mq.ListBrokersInput, ...func(*mq.Options)) (*mq.ListBrokersOutput, error) } -func RegisterMQTypes() { +func init() { gob.Register([]mqTypes.BrokerSummary{}) } diff --git a/aws/sdk/mq_mocks.go b/aws/sdk/mq_mocks.go new file mode 100644 index 0000000..4ae9b23 --- /dev/null +++ b/aws/sdk/mq_mocks.go @@ -0,0 +1,40 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/mq" + mqTypes "github.com/aws/aws-sdk-go-v2/service/mq/types" +) + +type MockedMQClient struct { +} + +func (m *MockedMQClient) ListBrokers(ctx context.Context, input *mq.ListBrokersInput, options ...func(*mq.Options)) (*mq.ListBrokersOutput, error) { + return &mq.ListBrokersOutput{ + BrokerSummaries: []mqTypes.BrokerSummary{ + { + BrokerArn: aws.String("broker1"), + BrokerId: aws.String("broker1"), + BrokerName: aws.String("broker1"), + BrokerState: mqTypes.BrokerStateRunning, + Created: aws.Time(time.Now()), + DeploymentMode: mqTypes.DeploymentModeSingleInstance, + EngineType: mqTypes.EngineTypeRabbitmq, + HostInstanceType: aws.String("host1"), + }, + { + BrokerArn: aws.String("broker2"), + BrokerId: aws.String("broker2"), + BrokerName: aws.String("broker2"), + BrokerState: mqTypes.BrokerStateRunning, + Created: aws.Time(time.Now()), + DeploymentMode: mqTypes.DeploymentModeSingleInstance, + EngineType: mqTypes.EngineTypeActivemq, + HostInstanceType: aws.String("host2"), + }, + }, + }, nil +} diff --git a/aws/sdk/opensearch.go b/aws/sdk/opensearch.go index 78a9ee1..42b762b 100644 --- a/aws/sdk/opensearch.go +++ b/aws/sdk/opensearch.go @@ -14,11 +14,13 @@ import ( type OpenSearchClientInterface interface { ListDomainNames(context.Context, *opensearch.ListDomainNamesInput, ...func(*opensearch.Options)) (*opensearch.ListDomainNamesOutput, error) DescribeDomainConfig(context.Context, *opensearch.DescribeDomainConfigInput, ...func(*opensearch.Options)) (*opensearch.DescribeDomainConfigOutput, error) + DescribeDomain(context.Context, *opensearch.DescribeDomainInput, ...func(*opensearch.Options)) (*opensearch.DescribeDomainOutput, error) } -func RegisterOpenSearchTypes() { +func init() { gob.Register([]openSearchTypes.DomainInfo{}) gob.Register(openSearchTypes.DomainConfig{}) + gob.Register(openSearchTypes.DomainStatus{}) } // create CachedOpenSearchListDomainNames function that uses go-cache and pagination @@ -75,3 +77,31 @@ func CachedOpenSearchDescribeDomainConfig(client OpenSearchClientInterface, acco internal.Cache.Set(cacheKey, DomainConfig, cache.DefaultExpiration) return DomainConfig, nil } + +// create CachedOpenSearchDescribeDomain function that uses go-cache and pagination and supports region option +func CachedOpenSearchDescribeDomain(client OpenSearchClientInterface, accountID string, region string, domainName string) (openSearchTypes.DomainStatus, error) { + var DomainStatus openSearchTypes.DomainStatus + cacheKey := fmt.Sprintf("%s-opensearch-DescribeDomain-%s-%s", accountID, region, domainName) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.(openSearchTypes.DomainStatus), nil + } + DescribeDomain, err := client.DescribeDomain( + context.TODO(), + &opensearch.DescribeDomainInput{ + DomainName: &domainName, + }, + func(o *opensearch.Options) { + o.Region = region + }, + ) + + if err != nil { + return DomainStatus, err + } + + DomainStatus = *DescribeDomain.DomainStatus + + internal.Cache.Set(cacheKey, DomainStatus, cache.DefaultExpiration) + return DomainStatus, nil +} diff --git a/aws/sdk/opensearch_mocks.go b/aws/sdk/opensearch_mocks.go new file mode 100644 index 0000000..4da3189 --- /dev/null +++ b/aws/sdk/opensearch_mocks.go @@ -0,0 +1,63 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/opensearch" + openSearchTypes "github.com/aws/aws-sdk-go-v2/service/opensearch/types" +) + +type MockedOpenSearchClient struct { +} + +func (m *MockedOpenSearchClient) ListDomainNames(ctx context.Context, input *opensearch.ListDomainNamesInput, options ...func(*opensearch.Options)) (*opensearch.ListDomainNamesOutput, error) { + return &opensearch.ListDomainNamesOutput{ + DomainNames: []openSearchTypes.DomainInfo{ + { + DomainName: aws.String("domain1"), + EngineType: openSearchTypes.EngineTypeOpenSearch, + }, + { + DomainName: aws.String("domain2"), + EngineType: openSearchTypes.EngineTypeElasticsearch, + }, + }, + }, nil +} + +func (m *MockedOpenSearchClient) DescribeDomainConfig(ctx context.Context, input *opensearch.DescribeDomainConfigInput, options ...func(*opensearch.Options)) (*opensearch.DescribeDomainConfigOutput, error) { + return &opensearch.DescribeDomainConfigOutput{ + DomainConfig: &openSearchTypes.DomainConfig{ + EngineVersion: &openSearchTypes.VersionStatus{ + Options: aws.String("OpenSearch-1.1"), + Status: &openSearchTypes.OptionStatus{ + PendingDeletion: aws.Bool(false), + }, + }, + ClusterConfig: &openSearchTypes.ClusterConfigStatus{ + Options: &openSearchTypes.ClusterConfig{ + DedicatedMasterCount: aws.Int32(3), + DedicatedMasterEnabled: aws.Bool(true), + InstanceCount: aws.Int32(3), + WarmCount: aws.Int32(3), + WarmEnabled: aws.Bool(true), + }, + }, + DomainEndpointOptions: &openSearchTypes.DomainEndpointOptionsStatus{ + Options: &openSearchTypes.DomainEndpointOptions{ + EnforceHTTPS: aws.Bool(true), + }, + }, + }, + }, nil +} + +func (m *MockedOpenSearchClient) DescribeDomain(ctx context.Context, input *opensearch.DescribeDomainInput, options ...func(*opensearch.Options)) (*opensearch.DescribeDomainOutput, error) { + return &opensearch.DescribeDomainOutput{ + DomainStatus: &openSearchTypes.DomainStatus{ + DomainName: aws.String("domain1"), + Endpoint: aws.String("https://domain1.us-east-1.es.amazonaws.com"), + }, + }, nil +} diff --git a/aws/sdk/org.go b/aws/sdk/org.go index a0d0de4..78df361 100644 --- a/aws/sdk/org.go +++ b/aws/sdk/org.go @@ -16,7 +16,7 @@ type OrganizationsClientInterface interface { DescribeOrganization(ctx context.Context, params *organizations.DescribeOrganizationInput, optFns ...func(*organizations.Options)) (*organizations.DescribeOrganizationOutput, error) } -func RegisterOrganizationsTypes() { +func init() { gob.Register([]orgTypes.Account{}) //gob.Register(orgTypes.Organization{}) } diff --git a/aws/sdk/org_mocks.go b/aws/sdk/org_mocks.go new file mode 100644 index 0000000..2b3a43e --- /dev/null +++ b/aws/sdk/org_mocks.go @@ -0,0 +1,52 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/organizations" + organizationsTypes "github.com/aws/aws-sdk-go-v2/service/organizations/types" +) + +type MockedOrgClient struct { +} + +func (m *MockedOrgClient) ListAccounts(ctx context.Context, input *organizations.ListAccountsInput, options ...func(*organizations.Options)) (*organizations.ListAccountsOutput, error) { + return &organizations.ListAccountsOutput{ + Accounts: []organizationsTypes.Account{ + { + Arn: aws.String("arn:aws:organizations::123456789012:account/o-exampleorgid/111111111111"), + Email: aws.String("unittesting@bishopfox.com"), + Id: aws.String("111111111111"), + JoinedMethod: organizationsTypes.AccountJoinedMethodInvited, + JoinedTimestamp: aws.Time(time.Now()), + Name: aws.String("account1"), + Status: organizationsTypes.AccountStatusActive, + }, + { + Arn: aws.String("arn:aws:organizations::123456789012:account/o-exampleorgid/222222222222"), + Email: aws.String("unittesting1@bishopfox.com"), + Id: aws.String("222222222222"), + JoinedMethod: organizationsTypes.AccountJoinedMethodCreated, + JoinedTimestamp: aws.Time(time.Now()), + Name: aws.String("account2"), + Status: organizationsTypes.AccountStatusActive, + }, + }, + }, nil + +} + +func (m *MockedOrgClient) DescribeOrganization(ctx context.Context, input *organizations.DescribeOrganizationInput, options ...func(*organizations.Options)) (*organizations.DescribeOrganizationOutput, error) { + return &organizations.DescribeOrganizationOutput{ + Organization: &organizationsTypes.Organization{ + Arn: aws.String("arn:aws:organizations::123456789012:organization/o-exampleorgid"), + FeatureSet: organizationsTypes.OrganizationFeatureSetAll, + Id: aws.String("o-exampleorgid"), + MasterAccountArn: aws.String("arn:aws:organizations::123456789012:account/o-exampleorgid/111111111111"), + MasterAccountEmail: aws.String("unittesting1@bishopfox.com"), + MasterAccountId: aws.String("111111111111"), + }, + }, nil +} diff --git a/aws/sdk/rds.go b/aws/sdk/rds.go index 611e232..bcb4d51 100644 --- a/aws/sdk/rds.go +++ b/aws/sdk/rds.go @@ -16,7 +16,7 @@ type RDSClientInterface interface { DescribeDBInstances(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) } -func RegisterRDSTypes() { +func init() { gob.Register([]rdsTypes.DBInstance{}) } diff --git a/aws/sdk/rds_mocks.go b/aws/sdk/rds_mocks.go new file mode 100644 index 0000000..f9b7907 --- /dev/null +++ b/aws/sdk/rds_mocks.go @@ -0,0 +1,34 @@ +package sdk + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/rds" + rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types" +) + +type MOckedRDSClient struct { +} + +func (m *MOckedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.DescribeDBInstancesInput, options ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) { + return &rds.DescribeDBInstancesOutput{ + DBInstances: []rdsTypes.DBInstance{ + { + DBInstanceIdentifier: aws.String("db1"), + Engine: aws.String("postgres"), + EngineVersion: aws.String("13.3"), + InstanceCreateTime: aws.Time(time.Now()), + MasterUsername: aws.String("postgres"), + }, + { + DBInstanceIdentifier: aws.String("db2"), + Engine: aws.String("postgres"), + EngineVersion: aws.String("13.3"), + InstanceCreateTime: aws.Time(time.Now()), + MasterUsername: aws.String("postgres"), + }, + }, + }, nil +} diff --git a/aws/sdk/redshift.go b/aws/sdk/redshift.go index ecd8a14..8604d42 100644 --- a/aws/sdk/redshift.go +++ b/aws/sdk/redshift.go @@ -11,15 +11,16 @@ import ( "github.com/patrickmn/go-cache" ) -type RedShiftClientInterface interface { +type AWSRedShiftClientInterface interface { DescribeClusters(context.Context, *redshift.DescribeClustersInput, ...func(*redshift.Options)) (*redshift.DescribeClustersOutput, error) } -func RegisterRedShiftTypes() { +func init() { gob.Register([]redshiftTypes.Cluster{}) + } -func CachedRedShiftDescribeClusters(client RedShiftClientInterface, accountID string, region string) ([]redshiftTypes.Cluster, error) { +func CachedRedShiftDescribeClusters(client AWSRedShiftClientInterface, accountID string, region string) ([]redshiftTypes.Cluster, error) { var PaginationControl *string var clusters []redshiftTypes.Cluster cacheKey := fmt.Sprintf("%s-redshift-DescribeClusters-%s", accountID, region) diff --git a/aws/sdk/redshift_mocks.go b/aws/sdk/redshift_mocks.go new file mode 100644 index 0000000..733c289 --- /dev/null +++ b/aws/sdk/redshift_mocks.go @@ -0,0 +1,37 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/redshift" + redshiftTypes "github.com/aws/aws-sdk-go-v2/service/redshift/types" +) + +type MockedRedshiftClient struct { +} + +func (m *MockedRedshiftClient) DescribeClusters(ctx context.Context, input *redshift.DescribeClustersInput, options ...func(*redshift.Options)) (*redshift.DescribeClustersOutput, error) { + return &redshift.DescribeClustersOutput{ + Clusters: []redshiftTypes.Cluster{ + { + ClusterIdentifier: aws.String("cluster1"), + ClusterNamespaceArn: aws.String("arn:aws:redshift:us-east-1:123456789012:cluster:cluster1"), + DBName: aws.String("db1"), + Endpoint: &redshiftTypes.Endpoint{ + Address: aws.String("cluster1.us-east-1.redshift.amazonaws.com"), + Port: aws.Int32(5439), + }, + }, + { + ClusterIdentifier: aws.String("cluster2"), + ClusterNamespaceArn: aws.String("arn:aws:redshift:us-east-1:123456789012:cluster:cluster2"), + DBName: aws.String("db2"), + Endpoint: &redshiftTypes.Endpoint{ + Address: aws.String("cluster2.us-east-1.redshift.amazonaws.com"), + Port: aws.Int32(5439), + }, + }, + }, + }, nil +} diff --git a/aws/sdk/route53.go b/aws/sdk/route53.go new file mode 100644 index 0000000..b54fec2 --- /dev/null +++ b/aws/sdk/route53.go @@ -0,0 +1,92 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/route53" + route53types "github.com/aws/aws-sdk-go-v2/service/route53/types" + "github.com/patrickmn/go-cache" +) + +type AWSRoute53ClientInterface interface { + ListHostedZones(context.Context, *route53.ListHostedZonesInput, ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) + ListResourceRecordSets(context.Context, *route53.ListResourceRecordSetsInput, ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) +} + +func init() { + gob.Register([]route53types.HostedZone{}) + gob.Register([]route53types.ResourceRecordSet{}) + +} + +func CachedRoute53ListHostedZones(client AWSRoute53ClientInterface, accountID string) ([]route53types.HostedZone, error) { + var PaginationControl *string + var hostedZones []route53types.HostedZone + cacheKey := fmt.Sprintf("%s-route53-ListHostedZones", accountID) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]route53types.HostedZone), nil + } + + for { + ListHostedZones, err := client.ListHostedZones( + context.TODO(), + &route53.ListHostedZonesInput{ + Marker: PaginationControl, + }, + ) + + if err != nil { + return hostedZones, err + } + + hostedZones = append(hostedZones, ListHostedZones.HostedZones...) + + //pagination + if ListHostedZones.Marker == nil { + break + } + PaginationControl = ListHostedZones.Marker + } + internal.Cache.Set(cacheKey, hostedZones, cache.DefaultExpiration) + return hostedZones, nil +} + +func CachedRoute53ListResourceRecordSets(client AWSRoute53ClientInterface, accountID string, hostedZoneID string) ([]route53types.ResourceRecordSet, error) { + var PaginationControl *string + var resourceRecordSets []route53types.ResourceRecordSet + // remove the /hostedzone/ prefix + hostedZoneID = hostedZoneID[12:] + cacheKey := fmt.Sprintf("%s-route53-ListResourceRecordSets-%s", accountID, hostedZoneID) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.([]route53types.ResourceRecordSet), nil + } + + for { + ListResourceRecordSets, err := client.ListResourceRecordSets( + context.TODO(), + &route53.ListResourceRecordSetsInput{ + HostedZoneId: &hostedZoneID, + StartRecordName: PaginationControl, + }, + ) + + if err != nil { + return resourceRecordSets, err + } + + resourceRecordSets = append(resourceRecordSets, ListResourceRecordSets.ResourceRecordSets...) + + //pagination + if !ListResourceRecordSets.IsTruncated { + break + } + PaginationControl = ListResourceRecordSets.NextRecordName + } + internal.Cache.Set(cacheKey, resourceRecordSets, cache.DefaultExpiration) + return resourceRecordSets, nil +} diff --git a/aws/sdk/route53_mocks.go b/aws/sdk/route53_mocks.go new file mode 100644 index 0000000..8a35646 --- /dev/null +++ b/aws/sdk/route53_mocks.go @@ -0,0 +1,70 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/route53" + route53Types "github.com/aws/aws-sdk-go-v2/service/route53/types" +) + +type MockedRoute53Client struct { +} + +func (m *MockedRoute53Client) ListHostedZones(ctx context.Context, input *route53.ListHostedZonesInput, options ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) { + return &route53.ListHostedZonesOutput{ + HostedZones: []route53Types.HostedZone{ + { + Id: aws.String("/hostedzone/zone1"), + Name: aws.String("zone1"), + ResourceRecordSetCount: aws.Int64(3), + }, + { + Id: aws.String("/hostedzone/zone2"), + Name: aws.String("zone2"), + ResourceRecordSetCount: aws.Int64(3), + }, + }, + }, nil +} + +func (m *MockedRoute53Client) ListResourceRecordSets(ctx context.Context, input *route53.ListResourceRecordSetsInput, options ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) { + return &route53.ListResourceRecordSetsOutput{ + ResourceRecordSets: []route53Types.ResourceRecordSet{ + { + Name: aws.String("zone1"), + Type: route53Types.RRTypeSoa, + }, + { + Name: aws.String("zone1"), + Type: route53Types.RRTypeNs, + }, + { + Name: aws.String("zone1"), + Type: route53Types.RRTypeA, + ResourceRecords: []route53Types.ResourceRecord{ + { + Value: aws.String("unit-test"), + }, + }, + }, + { + Name: aws.String("zone2"), + Type: route53Types.RRTypeSoa, + }, + { + Name: aws.String("zone2"), + Type: route53Types.RRTypeNs, + }, + { + Name: aws.String("zone2"), + Type: route53Types.RRTypeA, + ResourceRecords: []route53Types.ResourceRecord{ + { + Value: aws.String("unit-test"), + }, + }, + }, + }, + }, nil +} diff --git a/aws/sdk/s3.go b/aws/sdk/s3.go index 3b05374..da11d76 100644 --- a/aws/sdk/s3.go +++ b/aws/sdk/s3.go @@ -19,7 +19,7 @@ type AWSS3ClientInterface interface { GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) } -func RegisterS3Types() { +func init() { gob.Register([]s3Types.Bucket{}) gob.Register(s3Types.Bucket{}) gob.Register(&s3Types.PublicAccessBlockConfiguration{}) @@ -90,6 +90,7 @@ func CachedGetBucketPolicy(S3Client AWSS3ClientInterface, accountID string, r st }, ) if err != nil { + internal.Cache.Set(cacheKey, "", cache.DefaultExpiration) return "", err } diff --git a/aws/sdk/s3_mocks.go b/aws/sdk/s3_mocks.go new file mode 100644 index 0000000..1d79ed9 --- /dev/null +++ b/aws/sdk/s3_mocks.go @@ -0,0 +1,75 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3Types "github.com/aws/aws-sdk-go-v2/service/s3/types" +) + +type MockedS3Client struct { +} + +func (m *MockedS3Client) ListBuckets(ctx context.Context, input *s3.ListBucketsInput, options ...func(*s3.Options)) (*s3.ListBucketsOutput, error) { + return &s3.ListBucketsOutput{ + Buckets: []s3Types.Bucket{ + { + Name: aws.String("bucket1"), + }, + { + Name: aws.String("bucket2"), + }, + }, + }, nil +} + +func (m *MockedS3Client) GetBucketLocation(ctx context.Context, input *s3.GetBucketLocationInput, options ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) { + return &s3.GetBucketLocationOutput{ + LocationConstraint: s3Types.BucketLocationConstraintUsWest1, + }, nil +} + +func (m *MockedS3Client) GetBucketPolicy(ctx context.Context, input *s3.GetBucketPolicyInput, options ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error) { + return &s3.GetBucketPolicyOutput{ + Policy: aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AWSCloudTrailAclCheck20150319", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Action": "s3:GetBucketAcl", + "Resource": "arn:aws:s3:::bucket1" + }, + { + "Sid": "AWSCloudTrailWrite20150319", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::bucket1/AWSLogs/123456789012/*", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } + } + } + ] + }`), + }, nil +} + +func (m *MockedS3Client) GetPublicAccessBlock(ctx context.Context, input *s3.GetPublicAccessBlockInput, options ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) { + return &s3.GetPublicAccessBlockOutput{ + PublicAccessBlockConfiguration: &s3Types.PublicAccessBlockConfiguration{ + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + }, nil +} diff --git a/aws/sdk/secretsmanager.go b/aws/sdk/secretsmanager.go index d78022d..af8eefc 100644 --- a/aws/sdk/secretsmanager.go +++ b/aws/sdk/secretsmanager.go @@ -6,6 +6,8 @@ import ( "fmt" "github.com/BishopFox/cloudfox/internal" + "github.com/BishopFox/cloudfox/internal/aws/policy" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" secretsmanagerTypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" "github.com/patrickmn/go-cache" @@ -13,10 +15,13 @@ import ( type SecretsManagerClientInterface interface { ListSecrets(context.Context, *secretsmanager.ListSecretsInput, ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) + GetResourcePolicy(context.Context, *secretsmanager.GetResourcePolicyInput, ...func(*secretsmanager.Options)) (*secretsmanager.GetResourcePolicyOutput, error) } -func RegisterSecretsManagerTypes() { +func init() { gob.Register([]secretsmanagerTypes.SecretListEntry{}) + gob.Register(policy.Policy{}) + } func CachedSecretsManagerListSecrets(client SecretsManagerClientInterface, accountID string, region string) ([]secretsmanagerTypes.SecretListEntry, error) { @@ -54,3 +59,33 @@ func CachedSecretsManagerListSecrets(client SecretsManagerClientInterface, accou internal.Cache.Set(cacheKey, secrets, cache.DefaultExpiration) return secrets, nil } + +func CachedSecretsManagerGetResourcePolicy(client SecretsManagerClientInterface, secretId string, region string, accountID string) (policy.Policy, error) { + var secretPolicy policy.Policy + var policyJSON string + cacheKey := fmt.Sprintf("%s-secretsmanager-GetResourcePolicy-%s-%s", accountID, region, secretId) + cached, found := internal.Cache.Get(cacheKey) + if found { + return cached.(policy.Policy), nil + } + GetResourcePolicy, err := client.GetResourcePolicy( + context.TODO(), + &secretsmanager.GetResourcePolicyInput{ + SecretId: &secretId, + }, + func(o *secretsmanager.Options) { + o.Region = region + }, + ) + if err != nil { + return secretPolicy, err + } + + policyJSON = aws.ToString(GetResourcePolicy.ResourcePolicy) + secretPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON)) + if err != nil { + return secretPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", secretId, err) + } + internal.Cache.Set(cacheKey, secretPolicy, cache.DefaultExpiration) + return secretPolicy, nil +} diff --git a/aws/sdk/secretsmanager_mocks.go b/aws/sdk/secretsmanager_mocks.go new file mode 100644 index 0000000..b53f679 --- /dev/null +++ b/aws/sdk/secretsmanager_mocks.go @@ -0,0 +1,60 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + secretsmanagerTypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" +) + +type MockedSecretsManagerClient struct { +} + +func (m *MockedSecretsManagerClient) ListSecrets(ctx context.Context, input *secretsmanager.ListSecretsInput, options ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error) { + return &secretsmanager.ListSecretsOutput{ + SecretList: []secretsmanagerTypes.SecretListEntry{ + { + Name: aws.String("secret1"), + }, + { + Name: aws.String("secret2"), + }, + }, + }, nil +} + +func (m *MockedSecretsManagerClient) GetResourcePolicy(ctx context.Context, input *secretsmanager.GetResourcePolicyInput, options ...func(*secretsmanager.Options)) (*secretsmanager.GetResourcePolicyOutput, error) { + return &secretsmanager.GetResourcePolicyOutput{ + ResourcePolicy: aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "RetrieveSecret", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::123456789012:root" + }, + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "secretsmanager:ListSecretVersionIds" + ], + "Resource": "*" + }, + { + "Sid": "RetrieveSecret", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::123456789012:root" + }, + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + ], + "Resource": "*" + } + ] + }`), + }, nil +} diff --git a/aws/sdk/shared.go b/aws/sdk/shared.go index b059537..e9beb0e 100644 --- a/aws/sdk/shared.go +++ b/aws/sdk/shared.go @@ -1,5 +1,18 @@ package sdk -import "github.com/BishopFox/cloudfox/internal" +import ( + "log" + "os" + + "github.com/BishopFox/cloudfox/internal" +) var sharedLogger = internal.TxtLogger() + +func readTestFile(testFile string) []byte { + file, err := os.ReadFile(testFile) + if err != nil { + log.Fatalf("can't read file %s", testFile) + } + return file +} diff --git a/aws/sdk/sns.go b/aws/sdk/sns.go new file mode 100644 index 0000000..05bcdb7 --- /dev/null +++ b/aws/sdk/sns.go @@ -0,0 +1,181 @@ +package sdk + +import ( + "context" + "encoding/gob" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/sns" + snsTypes "github.com/aws/aws-sdk-go-v2/service/sns/types" + "github.com/patrickmn/go-cache" +) + +type AWSSNSClientInterface interface { + ListTopics(ctx context.Context, params *sns.ListTopicsInput, optFns ...func(*sns.Options)) (*sns.ListTopicsOutput, error) + ListSubscriptions(ctx context.Context, params *sns.ListSubscriptionsInput, optFns ...func(*sns.Options)) (*sns.ListSubscriptionsOutput, error) + ListSubscriptionsByTopic(ctx context.Context, params *sns.ListSubscriptionsByTopicInput, optFns ...func(*sns.Options)) (*sns.ListSubscriptionsByTopicOutput, error) + GetTopicAttributes(ctx context.Context, params *sns.GetTopicAttributesInput, optFns ...func(*sns.Options)) (*sns.GetTopicAttributesOutput, error) +} + +func init() { + gob.Register([]string{}) + gob.Register(snsTypes.Topic{}) + gob.Register(snsTypes.Subscription{}) +} + +func CachedSNSListTopics(SNSClient AWSSNSClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var topics []string + cacheKey := "sns-ListTopics-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached SNS topics data") + return cached.([]string), nil + } + + for { + ListTopics, err := SNSClient.ListTopics( + context.TODO(), + &sns.ListTopicsInput{ + NextToken: PaginationControl, + }, + func(o *sns.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + for _, topic := range ListTopics.Topics { + topics = append(topics, *topic.TopicArn) + } + + // Pagination control. + if ListTopics.NextToken != nil { + PaginationControl = ListTopics.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, topics, cache.DefaultExpiration) + + return topics, nil +} + +func CachedSNSListSubscriptions(SNSClient AWSSNSClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var subscriptions []string + cacheKey := "sns-ListSubscriptions-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached SNS subscriptions data") + return cached.([]string), nil + } + + for { + ListSubscriptions, err := SNSClient.ListSubscriptions( + context.TODO(), + &sns.ListSubscriptionsInput{ + NextToken: PaginationControl, + }, + func(o *sns.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + for _, subscription := range ListSubscriptions.Subscriptions { + subscriptions = append(subscriptions, *subscription.SubscriptionArn) + } + + // Pagination control. + if ListSubscriptions.NextToken != nil { + PaginationControl = ListSubscriptions.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, subscriptions, cache.DefaultExpiration) + + return subscriptions, nil +} + +func CachedSNSListSubscriptionsByTopic(SNSClient AWSSNSClientInterface, accountID string, region string, topicArn string) ([]string, error) { + var PaginationControl *string + var subscriptions []string + cacheKey := "sns-ListSubscriptionsByTopic-" + accountID + "-" + region + "-" + topicArn + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached SNS subscriptions data") + return cached.([]string), nil + } + + for { + ListSubscriptionsByTopic, err := SNSClient.ListSubscriptionsByTopic( + context.TODO(), + &sns.ListSubscriptionsByTopicInput{ + NextToken: PaginationControl, + TopicArn: &topicArn, + }, + func(o *sns.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + for _, subscription := range ListSubscriptionsByTopic.Subscriptions { + subscriptions = append(subscriptions, *subscription.SubscriptionArn) + } + + // Pagination control. + if ListSubscriptionsByTopic.NextToken != nil { + PaginationControl = ListSubscriptionsByTopic.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, subscriptions, cache.DefaultExpiration) + + return subscriptions, nil +} + +func CachedSNSGetTopicAttributes(SNSClient AWSSNSClientInterface, accountID string, region string, topicArn string) (map[string]string, error) { + cacheKey := "sns-GetTopicAttributes-" + accountID + "-" + region + "-" + topicArn + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached SNS topic attributes data") + return cached.(map[string]string), nil + } + + GetTopicAttributes, err := SNSClient.GetTopicAttributes( + context.TODO(), + &sns.GetTopicAttributesInput{ + TopicArn: &topicArn, + }, + func(o *sns.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + return nil, err + } + + internal.Cache.Set(cacheKey, GetTopicAttributes.Attributes, cache.DefaultExpiration) + + return GetTopicAttributes.Attributes, nil +} diff --git a/aws/sdk/sns_mocks.go b/aws/sdk/sns_mocks.go new file mode 100644 index 0000000..2d208f4 --- /dev/null +++ b/aws/sdk/sns_mocks.go @@ -0,0 +1,60 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sns" + snsTypes "github.com/aws/aws-sdk-go-v2/service/sns/types" +) + +type MockedSNSClient struct { +} + +func (m *MockedSNSClient) ListTopics(ctx context.Context, input *sns.ListTopicsInput, options ...func(*sns.Options)) (*sns.ListTopicsOutput, error) { + return &sns.ListTopicsOutput{ + Topics: []snsTypes.Topic{ + { + TopicArn: aws.String("arn:aws:sns:us-east-1:123456789012:topic1"), + }, + { + TopicArn: aws.String("arn:aws:sns:us-east-1:123456789012:topic2"), + }, + }, + }, nil +} + +func (m *MockedSNSClient) ListSubscriptionsByTopic(ctx context.Context, input *sns.ListSubscriptionsByTopicInput, options ...func(*sns.Options)) (*sns.ListSubscriptionsByTopicOutput, error) { + return &sns.ListSubscriptionsByTopicOutput{ + Subscriptions: []snsTypes.Subscription{ + { + SubscriptionArn: aws.String("arn:aws:sns:us-east-1:123456789012:topic1:sub1"), + }, + { + SubscriptionArn: aws.String("arn:aws:sns:us-east-1:123456789012:topic1:sub2"), + }, + }, + }, nil +} + +func (m *MockedSNSClient) ListSubscriptions(ctx context.Context, input *sns.ListSubscriptionsInput, options ...func(*sns.Options)) (*sns.ListSubscriptionsOutput, error) { + return &sns.ListSubscriptionsOutput{ + Subscriptions: []snsTypes.Subscription{ + { + SubscriptionArn: aws.String("arn:aws:sns:us-east-1:123456789012:topic1:sub1"), + }, + { + SubscriptionArn: aws.String("arn:aws:sns:us-east-1:123456789012:topic1:sub2"), + }, + }, + }, nil +} + +func (m *MockedSNSClient) GetTopicAttributes(ctx context.Context, input *sns.GetTopicAttributesInput, options ...func(*sns.Options)) (*sns.GetTopicAttributesOutput, error) { + return &sns.GetTopicAttributesOutput{ + Attributes: map[string]string{ + "DisplayName": "topic1", + "Policy": `{"Statement":[{"Effect":"Allow","Principal":"*","Action":"SNS:Publish","Resource":"arn:aws:sns:us-east-1:123456789012:topoic1","Condition":{"StringEquals":{"aws:sourceVpce":"vpce-1a2b3c4d"}}}]}`, + }, + }, nil +} diff --git a/aws/sdk/sqs.go b/aws/sdk/sqs.go new file mode 100644 index 0000000..4b46dbf --- /dev/null +++ b/aws/sdk/sqs.go @@ -0,0 +1,58 @@ +package sdk + +import ( + "context" + "encoding/gob" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/patrickmn/go-cache" +) + +type AWSSQSClientInterface interface { + ListQueues(ctx context.Context, params *sqs.ListQueuesInput, optFns ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error) +} + +func init() { + gob.Register([]string{}) +} + +func CachedSQSListQueues(SQSClient AWSSQSClientInterface, accountID string, region string) ([]string, error) { + var PaginationControl *string + var queues []string + cacheKey := "sqs-ListQueues-" + accountID + "-" + region + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached SQS queues data") + return cached.([]string), nil + } + + for { + ListQueues, err := SQSClient.ListQueues( + context.TODO(), + &sqs.ListQueuesInput{ + NextToken: PaginationControl, + }, + func(o *sqs.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + queues = append(queues, ListQueues.QueueUrls...) + + // Pagination control. + if ListQueues.NextToken != nil { + PaginationControl = ListQueues.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, queues, cache.DefaultExpiration) + return queues, nil +} diff --git a/aws/sdk/sqs_mocks.go b/aws/sdk/sqs_mocks.go new file mode 100644 index 0000000..7208c55 --- /dev/null +++ b/aws/sdk/sqs_mocks.go @@ -0,0 +1,28 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/sqs" +) + +type MockedSQSClient struct { +} + +func (m *MockedSQSClient) ListQueues(ctx context.Context, input *sqs.ListQueuesInput, options ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error) { + return &sqs.ListQueuesOutput{ + QueueUrls: []string{ + "https://sqs.us-east-1.amazonaws.com/123456789012/queue1", + "https://sqs.us-east-1.amazonaws.com/123456789012/queue2", + }, + }, nil +} + +func (m *MockedSQSClient) GetQueueAttributes(ctx context.Context, input *sqs.GetQueueAttributesInput, options ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error) { + return &sqs.GetQueueAttributesOutput{ + Attributes: map[string]string{ + "QueueArn": "arn:aws:sqs:us-east-1:123456789012:queue1", + "Policy": `{"Version": "2012-10-17","Id": "anyID","Statement": [{"Sid":"unconditionally_public","Effect": "Allow","Principal": {"AWS": "*"},"Action": "sqs:*","Resource": "arn:aws:sqs:*:123456789012:some-queue"}]}`, + }, + }, nil +} diff --git a/aws/sdk/ssm.go b/aws/sdk/ssm.go new file mode 100644 index 0000000..8ec4673 --- /dev/null +++ b/aws/sdk/ssm.go @@ -0,0 +1,61 @@ +package sdk + +import ( + "context" + "encoding/gob" + "fmt" + + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/patrickmn/go-cache" +) + +type AWSSSMClientInterface interface { + DescribeParameters(ctx context.Context, params *ssm.DescribeParametersInput, optFns ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) +} + +func init() { + gob.Register([]types.ParameterMetadata{}) +} + +// create a CachedSSMDescribeParameters function that uses go-cache line the other Cached* functions. It should accept a ssm client, account id, and region. Make sure it handles the region option and pagination if needed +func CachedSSMDescribeParameters(SSMClient AWSSSMClientInterface, accountID string, region string) ([]types.ParameterMetadata, error) { + var PaginationControl *string + var parameters []types.ParameterMetadata + cacheKey := fmt.Sprintf("%s-ssm-DescribeParameters-%s", accountID, region) + cached, found := internal.Cache.Get(cacheKey) + if found { + sharedLogger.Debug("Using cached SSM parameters data") + return cached.([]types.ParameterMetadata), nil + } + + for { + DescribeParameters, err := SSMClient.DescribeParameters( + context.TODO(), + &ssm.DescribeParametersInput{ + NextToken: PaginationControl, + }, + func(o *ssm.Options) { + o.Region = region + }, + ) + if err != nil { + sharedLogger.Error(err.Error()) + break + } + + parameters = append(parameters, DescribeParameters.Parameters...) + + // Pagination control. + if DescribeParameters.NextToken != nil { + PaginationControl = DescribeParameters.NextToken + } else { + PaginationControl = nil + break + } + } + + internal.Cache.Set(cacheKey, parameters, cache.DefaultExpiration) + return parameters, nil +} diff --git a/aws/sdk/ssm_mocks.go b/aws/sdk/ssm_mocks.go new file mode 100644 index 0000000..d28bde9 --- /dev/null +++ b/aws/sdk/ssm_mocks.go @@ -0,0 +1,27 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ssm" + ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types" +) + +type MockedSSMClient struct { +} + +func (m *MockedSSMClient) DescribeParameters(ctx context.Context, input *ssm.DescribeParametersInput, options ...func(*ssm.Options)) (*ssm.DescribeParametersOutput, error) { + return &ssm.DescribeParametersOutput{ + Parameters: []ssmTypes.ParameterMetadata{ + { + Name: aws.String("/parameter/param1"), + Type: ssmTypes.ParameterTypeString, + }, + { + Name: aws.String("/parameter/param2"), + Type: ssmTypes.ParameterTypeString, + }, + }, + }, nil +} diff --git a/aws/sdk/stepfunctions.go b/aws/sdk/stepfunctions.go index 37bb64f..c154c58 100644 --- a/aws/sdk/stepfunctions.go +++ b/aws/sdk/stepfunctions.go @@ -15,7 +15,7 @@ type StepFunctionsClientInterface interface { ListStateMachines(context.Context, *sfn.ListStateMachinesInput, ...func(*sfn.Options)) (*sfn.ListStateMachinesOutput, error) } -func RegisterStepFunctionsTypes() { +func init() { gob.Register([]sfnTypes.StateMachineListItem{}) } diff --git a/aws/sdk/stepfunctions_mocks.go b/aws/sdk/stepfunctions_mocks.go new file mode 100644 index 0000000..50bf55f --- /dev/null +++ b/aws/sdk/stepfunctions_mocks.go @@ -0,0 +1,27 @@ +package sdk + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sfn" + sfnTypes "github.com/aws/aws-sdk-go-v2/service/sfn/types" +) + +type MockedStepFunctionsClient struct { +} + +func (m *MockedStepFunctionsClient) ListStateMachines(ctx context.Context, input *sfn.ListStateMachinesInput, options ...func(*sfn.Options)) (*sfn.ListStateMachinesOutput, error) { + return &sfn.ListStateMachinesOutput{ + StateMachines: []sfnTypes.StateMachineListItem{ + { + Name: aws.String("state_machine1"), + StateMachineArn: aws.String("arn:aws:states:us-east-1:123456789012:stateMachine:state_machine1"), + }, + { + Name: aws.String("state_machine2"), + StateMachineArn: aws.String("arn:aws:states:us-east-1:123456789012:stateMachine:state_machine2"), + }, + }, + }, nil +} diff --git a/aws/secrets.go b/aws/secrets.go index 47b3f06..380b6c4 100644 --- a/aws/secrets.go +++ b/aws/secrets.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "sync" "github.com/BishopFox/cloudfox/internal" @@ -22,11 +23,13 @@ type SecretsModule struct { SecretsManagerClient *secretsmanager.Client SSMClient *ssm.Client - Caller sts.GetCallerIdentityOutput - AWSRegions []string - AWSProfile string - Goroutines int - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSProfile string + Goroutines int + WrapTable bool + AWSOutputType string + AWSTableCols string // Main module data Secrets []Secret @@ -45,8 +48,8 @@ type Secret struct { Description string } -func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *SecretsModule) PrintSecrets(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "secrets" @@ -94,17 +97,48 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string // fmt.Printf("\nAnalyzed Resources by Region\n\n") m.output.Headers = []string{ + "Account", "Service", "Region", "Name", "Description", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Service", + "Region", + "Name", + "Description", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Service", + "Region", + "Name", + "Description", + } + } + // Table rows for i := range m.Secrets { m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Secrets[i].AWSService, m.Secrets[i].Region, m.Secrets[i].Name, @@ -116,11 +150,7 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //m.output.OutputSelector(outputFormat) - //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -129,9 +159,10 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) diff --git a/aws/shared.go b/aws/shared.go index cde9e90..d0f16f7 100644 --- a/aws/shared.go +++ b/aws/shared.go @@ -18,9 +18,8 @@ var red = color.New(color.FgRed).SprintFunc() var yellow = color.New(color.FgRed).SprintFunc() var blue = color.New(color.FgBlue).SprintFunc() var magenta = color.New(color.FgMagenta).SprintFunc() - var green = color.New(color.FgGreen).SprintFunc() - +var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"} var sharedLogger = internal.TxtLogger() func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod IamSimulatorModule, localAdminMap map[string]bool) (string, string) { @@ -178,3 +177,12 @@ func GetResourceNameFromArn(arn string) string { return resourceName } + +func removeStringFromSlice(slice []string, element string) []string { + for i, v := range slice { + if v == element { + return append(slice[:i], slice[i+1:]...) + } + } + return slice +} diff --git a/aws/sns.go b/aws/sns.go index 9418947..1309914 100644 --- a/aws/sns.go +++ b/aws/sns.go @@ -29,9 +29,11 @@ type SNSModule struct { AWSProfile string Caller sts.GetCallerIdentityOutput StorePolicies bool - OutputFormat string - Goroutines int - WrapTable bool + AWSOutputType string + AWSTableCols string + + Goroutines int + WrapTable bool // Main module data Topics []SNSTopic @@ -61,8 +63,8 @@ type SNSTopic struct { ResourcePolicySummary string } -func (m *SNSModule) PrintSNS(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *SNSModule) PrintSNS(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "sns" @@ -108,12 +110,40 @@ func (m *SNSModule) PrintSNS(outputFormat string, outputDirectory string, verbos // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ - + "Account", "ARN", "Public?", "Resource Policy Summary", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "ARN", + "Public?", + "Resource Policy Summary", + } + + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "ARN", + "Public?", + "Resource Policy Summary", + } + } + sort.SliceStable(m.Topics, func(i, j int) bool { return m.Topics[i].ARN < m.Topics[j].ARN }) @@ -123,6 +153,7 @@ func (m *SNSModule) PrintSNS(outputFormat string, outputDirectory string, verbos m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Topics[i].ARN, m.Topics[i].IsPublic, m.Topics[i].ResourcePolicySummary, @@ -131,9 +162,7 @@ func (m *SNSModule) PrintSNS(outputFormat string, outputDirectory string, verbos } if len(m.output.Body) > 0 { - //m.output.OutputSelector(outputFormat) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -142,9 +171,10 @@ func (m *SNSModule) PrintSNS(outputFormat string, outputDirectory string, verbos }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -288,7 +318,7 @@ func (m *SNSModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphor if !topic.Policy.IsEmpty() { m.analyseTopicPolicy(topic, dataReceiver) } else { - // If the topic policy "resource policy" is empty, the only principals that have permisisons + // If the topic policy "resource policy" is empty, the only principals that have permissions // 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" diff --git a/aws/sns_test.go b/aws/sns_test.go index d2e4b71..f07e01d 100644 --- a/aws/sns_test.go +++ b/aws/sns_test.go @@ -42,9 +42,9 @@ func TestSNSQueues(t *testing.T) { tmpDir := "." // execute the module with verbosity = 2 - m.PrintSNS("table", tmpDir, 2) + m.PrintSNS(tmpDir, 2) - resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/sns.txt") + resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/sns.txt") resultsFile, err := afero.ReadFile(fs, resultsFilePath) if err != nil { t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err) diff --git a/aws/sqs.go b/aws/sqs.go index d3efaca..77abf66 100644 --- a/aws/sqs.go +++ b/aws/sqs.go @@ -28,12 +28,14 @@ type SQSModule struct { StorePolicies bool - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string - Goroutines int - AWSProfile string - WrapTable bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + + Goroutines int + AWSProfile string + WrapTable bool // Main module data Queues []Queue @@ -64,8 +66,8 @@ type Queue struct { ResourcePolicySummary string } -func (m *SQSModule) PrintSQS(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *SQSModule) PrintSQS(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "sqs" @@ -111,11 +113,40 @@ func (m *SQSModule) PrintSQS(outputFormat string, outputDirectory string, verbos // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ + "Account", "Arn", "Public?", "Resource Policy Summary", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Arn", + "Public?", + "Resource Policy Summary", + } + + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Arn", + "Public?", + "Resource Policy Summary", + } + } + sort.SliceStable(m.Queues, func(i, j int) bool { return m.Queues[i].URL < m.Queues[j].URL }) @@ -125,6 +156,7 @@ func (m *SQSModule) PrintSQS(outputFormat string, outputDirectory string, verbos m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Queues[i].Arn, m.Queues[i].IsPublic, m.Queues[i].ResourcePolicySummary, @@ -133,9 +165,7 @@ func (m *SQSModule) PrintSQS(outputFormat string, outputDirectory string, verbos } if len(m.output.Body) > 0 { - //m.output.OutputSelector(outputFormat) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -144,9 +174,10 @@ func (m *SQSModule) PrintSQS(outputFormat string, outputDirectory string, verbos }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -283,7 +314,7 @@ func (m *SQSModule) getSQSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho if !queue.Policy.IsEmpty() { m.analyseQueuePolicy(queue, dataReceiver) } else { - // If the queue policy "resource policy" is empty, the only principals that have permisisons + // If the queue policy "resource policy" is empty, the only principals that have permissions // 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" @@ -390,7 +421,7 @@ func (m *SQSModule) analyseQueuePolicy(queue *Queue, dataReceiver chan Queue) { } else { queue.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account) } - queue.ResourcePolicySummary = strings.TrimSuffix(queue.ResourcePolicySummary, "\n") + //queue.ResourcePolicySummary = strings.TrimSuffix(queue.ResourcePolicySummary, "\n") } dataReceiver <- *queue diff --git a/aws/sqs_test.go b/aws/sqs_test.go index 84b296b..59ff43e 100644 --- a/aws/sqs_test.go +++ b/aws/sqs_test.go @@ -48,9 +48,9 @@ func TestSQSQueues(t *testing.T) { tmpDir := "." // execute the module with verbosity set to 2 - m.PrintSQS("table", tmpDir, 2) + m.PrintSQS(tmpDir, 2) - resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/sqs.txt") + resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/sqs.txt") resultsFile, err := afero.ReadFile(fs, resultsFilePath) if err != nil { t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err) diff --git a/aws/tags.go b/aws/tags.go index 0e7306b..e32aba2 100644 --- a/aws/tags.go +++ b/aws/tags.go @@ -26,9 +26,11 @@ type TagsModule struct { // General configuration data ResourceGroupsTaggingApiInterface TagsGetResourcesAPI - Caller sts.GetCallerIdentityOutput - AWSRegions []string - OutputFormat string + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + Goroutines int AWSProfile string WrapTable bool @@ -54,8 +56,8 @@ type Tag struct { Value string } -func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verbosity int) { - // These stuct values are used by the output module +func (m *TagsModule) PrintTags(outputDirectory string, verbosity int) { + // These struct values are used by the output module m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "tags" @@ -102,6 +104,7 @@ func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verb // add - if struct is not empty do this. otherwise, dont write anything. m.output.Headers = []string{ + "Account", "Service", "Region", "Type", @@ -111,6 +114,41 @@ func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verb "Value", } + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Service", + "Region", + "Type", + //"Name", + //"Resource Arn", + "Key", + "Value", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Service", + "Region", + "Type", + //"Name", + //"Resource Arn", + "Key", + "Value", + } + } + sort.Slice(m.Tags, func(i, j int) bool { return m.Tags[i].AWSService < m.Tags[j].AWSService }) @@ -121,6 +159,7 @@ func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verb m.output.Body = append( m.output.Body, []string{ + aws.ToString(m.Caller.Account), m.Tags[i].AWSService, m.Tags[i].Region, m.Tags[i].Type, @@ -134,9 +173,7 @@ func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verb } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - //utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule) - //internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile) - //m.writeLoot(m.output.FilePath, verbosity) + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -145,9 +182,10 @@ func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verb }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: m.output.Headers, - Body: m.output.Body, - Name: m.output.CallingModule, + Header: m.output.Headers, + Body: m.output.Body, + TableCols: tableCols, + Name: m.output.CallingModule, }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) @@ -284,7 +322,7 @@ 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. + // a for loop that accepts user input. If no user input, it will continue to paginate until there are no more pages. If there is user input, it will paginate until the user input is reached. for { if len(resources) < m.MaxResourcesPerRegion || m.MaxResourcesPerRegion == 0 { diff --git a/aws/tags_test.go b/aws/tags_test.go index 8575b7b..592e7f2 100644 --- a/aws/tags_test.go +++ b/aws/tags_test.go @@ -53,7 +53,6 @@ func TestTags(t *testing.T) { UserId: aws.String("AIDAJDPLRKLG7UEXAMPLE"), }, AWSRegions: []string{"us-east-1"}, - OutputFormat: "table", Goroutines: 10, AWSProfile: "test", WrapTable: false, @@ -73,7 +72,7 @@ func TestTags(t *testing.T) { 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) + subtest.testModule.PrintTags(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)) } diff --git a/azure/inventory.go b/azure/inventory.go index e5665a7..1fe9789 100644 --- a/azure/inventory.go +++ b/azure/inventory.go @@ -14,7 +14,7 @@ import ( "github.com/kyokomi/emoji" ) -func AzInventoryCommand(AzTenantID, AzSubscriptionID, Version string, AzVerbosity int, AzWrapTable bool) error { +func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error { o := internal.OutputClient{ Verbosity: AzVerbosity, CallingModule: globals.AZ_INVENTORY_MODULE_NAME, @@ -24,36 +24,55 @@ func AzInventoryCommand(AzTenantID, AzSubscriptionID, Version string, AzVerbosit } if AzTenantID != "" && AzSubscriptionID == "" { - // To-Do: implement per tentant - fmt.Println("Inventory per tenant not yet implemented. Please use the --subscription flag instead.") + // cloudfox azure inventory --tenant [TENANT_ID | PRIMARY_DOMAIN] + tenantInfo := populateTenant(AzTenantID) + + if AzMergedTable { + // set up table vars + var header []string + var body [][]string + + o := internal.OutputClient{ + Verbosity: AzVerbosity, + CallingModule: globals.AZ_INVENTORY_MODULE_NAME, + Table: internal.TableClient{ + Wrap: AzWrapTable, + }, + } - } else if AzTenantID == "" && AzSubscriptionID != "" { + fmt.Printf( + "[%s][%s] Gathering inventory for subscription %s\n", + color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(o.CallingModule), + fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID))) - // ./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) + o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain) + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level") - AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscriptionID)) + //populate the table data + header, body, err := getInventoryInfoPerTenant(ptr.ToString(tenantInfo.ID)) + if err != nil { + return err + } + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(o.CallingModule)}) + + if body != nil { + o.WriteFullOutput(o.Table.TableFiles, nil) + } + } else { - header, body, err := prepareInventoryTable(AzTenantID, AzSubscriptionID) - if err != nil { - return err + for _, s := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) { + runInventoryCommandForSingleSubscription(ptr.ToString(s.SubscriptionID), AzOutputDirectory, AzVerbosity, AzWrapTable, Version) + } } - 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 if AzTenantID == "" && AzSubscriptionID != "" { + + // ./cloudfox azure inventory --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME] + runInventoryCommandForSingleSubscription(AzSubscriptionID, AzOutputDirectory, AzVerbosity, AzWrapTable, Version) } else { // Error: please make a valid flag selection @@ -63,7 +82,51 @@ func AzInventoryCommand(AzTenantID, AzSubscriptionID, Version string, AzVerbosit return nil } -func prepareInventoryTable(tenantID, subscriptionID string) ([]string, [][]string, error) { +func runInventoryCommandForSingleSubscription(AzSubscription string, AzOutputDirectory string, AzVerbosity int, AzWrapTable bool, Version string) error { + // set up table vars + var header []string + var body [][]string + var err error + o := internal.OutputClient{ + Verbosity: AzVerbosity, + CallingModule: globals.AZ_INVENTORY_MODULE_NAME, + Table: internal.TableClient{ + Wrap: AzWrapTable, + }, + } + var AzSubscriptionInfo SubsriptionInfo + tenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription)) + tenantInfo := populateTenant(tenantID) + AzSubscriptionInfo = PopulateSubsriptionType(AzSubscription) + o.PrefixIdentifier = AzSubscriptionInfo.Name + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name) + + fmt.Printf( + "[%s][%s] Gathering inventory for subscription %s\n", + color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(o.CallingModule), + fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID)) + + // populate the table data + header, body, err = getInventoryInfoPerSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID) + if err != nil { + return err + } + + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(globals.AZ_INVENTORY_MODULE_NAME)}) + + if body != nil { + o.WriteFullOutput(o.Table.TableFiles, nil) + fmt.Println() + } + + return nil +} + +func getInventoryInfoPerSubscription(tenantID, subscriptionID string) ([]string, [][]string, error) { resources, err := getResources(tenantID, subscriptionID) if err != nil { return nil, nil, err @@ -104,11 +167,59 @@ func prepareInventoryTable(tenantID, subscriptionID string) ([]string, [][]strin } body = append(body, row) } - sort.Slice(body, func(i, j int) bool { return body[i][0] < body[j][0] }) + return header, body, nil +} + +func getInventoryInfoPerTenant(tenantID string) ([]string, [][]string, error) { + + inventory := make(map[string]map[string]int) + resourceTypes := make(map[string]bool) + resourceLocations := make(map[string]bool) + + for _, s := range GetSubscriptionsPerTenantID(tenantID) { + resources, err := getResources(tenantID, ptr.ToString(s.SubscriptionID)) + if err != nil { + return nil, nil, err + } + + 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 } diff --git a/azure/azurerbac.go b/azure/rbac.go similarity index 79% rename from azure/azurerbac.go rename to azure/rbac.go index 0340bd0..f4eb80f 100644 --- a/azure/azurerbac.go +++ b/azure/rbac.go @@ -7,10 +7,11 @@ import ( "log" "os" "path/filepath" + "sort" "strings" "github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization" - "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" + "github.com/Azure/azure-sdk-for-go/profiles/latest/graphrbac/graphrbac" "github.com/BishopFox/cloudfox/globals" "github.com/BishopFox/cloudfox/internal" "github.com/aws/smithy-go/ptr" @@ -18,38 +19,74 @@ import ( "github.com/kyokomi/emoji" ) -func AzRBACCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, AzVerbosity int, AzWrapTable bool) error { +func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error { + // setup logging client + o := internal.OutputClient{ + Verbosity: AzVerbosity, + CallingModule: globals.AZ_RBAC_MODULE_NAME, + Table: internal.TableClient{ + Wrap: AzWrapTable, + }, + } + // initiate command specific client var c CloudFoxRBACclient + // set up table vars var header []string var body [][]string - var outputDirectory, controlMessagePrefix string - if AzTenantID != "" && AzSubscriptionID == "" { - // ./cloudfox azure rbac --tenant TENANT_ID - fmt.Printf("[%s][%s] Enumerating RBAC permissions for tenant %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME), AzTenantID) - controlMessagePrefix = fmt.Sprintf("tenant-%s", AzTenantID) - outputDirectory = filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "tenants", AzTenantID) + var AzSubscriptionInfo SubsriptionInfo + + if AzTenantID != "" && AzSubscription == "" { + // cloudfox azure rbac --tenant [TENANT_ID | PRIMARY_DOMAIN] + var err error - header, body, err = getRBACperTenant(AzTenantID, c) + tenantInfo := populateTenant(AzTenantID) + if err != nil { + return err + } + o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain) + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level") + + fmt.Printf("[%s][%s] Enumerating RBAC permissions for tenant %s\n", + color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME), + fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID))) + + header, body, err = getRBACperTenant(ptr.ToString(tenantInfo.ID), c) if err != nil { return err } - } else if AzTenantID == "" && AzSubscriptionID != "" { - // ./cloudfox azure rbac --subscription SUBSCRIPTION_ID - fmt.Printf("[%s][%s] Enumerating RBAC permissions for subscription %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME), AzSubscriptionID) - controlMessagePrefix = fmt.Sprintf("subscription-%s", AzSubscriptionID) - outputDirectory = filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "subscriptions", AzSubscriptionID) - header, body = getRBACperSubscription(AzTenantID, AzSubscriptionID, c) + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(globals.AZ_RBAC_MODULE_NAME)}) + + } else if AzTenantID == "" && AzSubscription != "" { + // cloudfox azure rbac --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME] + tenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription)) + tenantInfo := populateTenant(tenantID) + AzSubscriptionInfo = PopulateSubsriptionType(AzSubscription) + o.PrefixIdentifier = AzSubscriptionInfo.Name + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name) + + fmt.Printf("[%s][%s] Enumerating RBAC permissions for subscription %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME), + fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID)) + header, body = getRBACperSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID, c) + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(globals.AZ_RBAC_MODULE_NAME)}) } else { // Error: please make a valid flag selection fmt.Println("Please enter a valid input with a valid flag. Use --help for info.") } - fileNameWithoutExtension := globals.AZ_RBAC_MODULE_NAME - if body != nil { - internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_RBAC_MODULE_NAME, AzWrapTable, controlMessagePrefix) + //internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_RBAC_MODULE_NAME, AzWrapTable, controlMessagePrefix) + o.WriteFullOutput(o.Table.TableFiles, nil) + } return nil } @@ -57,7 +94,7 @@ func AzRBACCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, func getRBACperTenant(AzTenantID string, c CloudFoxRBACclient) ([]string, [][]string, error) { var selectedSubs, resultsHeader []string var resultsBody, b [][]string - for _, s := range getSubscriptions() { + for _, s := range GetSubscriptions() { if ptr.ToString(s.TenantID) == AzTenantID { selectedSubs = append(selectedSubs, ptr.ToString(s.SubscriptionID)) } @@ -76,7 +113,7 @@ func getRBACperTenant(AzTenantID string, c CloudFoxRBACclient) ([]string, [][]st func getRBACperSubscription(AzTenantID, AzSubscriptionID string, c CloudFoxRBACclient) ([]string, [][]string) { var resultsHeader []string var resultsBody [][]string - for _, s := range getSubscriptions() { + for _, s := range GetSubscriptions() { if ptr.ToString(s.SubscriptionID) == AzSubscriptionID { c.initialize(AzTenantID, []string{ptr.ToString(s.SubscriptionID)}) resultsHeader, resultsBody = c.GetRelevantRBACData(AzTenantID, ptr.ToString(s.SubscriptionID)) @@ -132,8 +169,13 @@ func (c *CloudFoxRBACclient) GetRelevantRBACData(tenantID, subscriptionID string findRole(c.roleDefinitions, rb, &roleAssignmentRelevantData) results = append(results, roleAssignmentRelevantData) } + // Sort the results by userDisplayName using slice.Sort + sortedResults := results + sort.Slice(sortedResults, func(i, j int) bool { + return sortedResults[i].userDisplayName < sortedResults[j].userDisplayName + }) - for _, r := range results { + for _, r := range sortedResults { body = append(body, []string{ r.userDisplayName, diff --git a/azure/azurerbac_test.go b/azure/rbac_test.go similarity index 87% rename from azure/azurerbac_test.go rename to azure/rbac_test.go index 7ccfe7b..3abe9ca 100644 --- a/azure/azurerbac_test.go +++ b/azure/rbac_test.go @@ -20,18 +20,21 @@ func TestAzRBACCommand(t *testing.T) { azRGName string azVerbosity int azOutputFormat string + azOutputDirectory string version string resourcesTestFile string usersTestFile string roleDefinitionsTestFile string roleAssignmentsTestFile string wrapTableOutput bool + azMergedTable bool }{ { name: "./cloudfox azure rbac --tenant 11111111-1111-1111-1111-11111111", azTenantID: "11111111-1111-1111-1111-11111111", azSubscriptionID: "", azOutputFormat: "all", + azOutputDirectory: "~/.cloudfox", azVerbosity: 2, resourcesTestFile: "./test-data/resources.json", usersTestFile: "./test-data/users.json", @@ -39,12 +42,14 @@ func TestAzRBACCommand(t *testing.T) { roleAssignmentsTestFile: "./test-data/role-assignments.json", version: "DEV", wrapTableOutput: false, + azMergedTable: false, }, { name: "./cloudfox azure rbac --subscription AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA", azTenantID: "", azSubscriptionID: "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA", azOutputFormat: "all", + azOutputDirectory: "~/.cloudfox", azVerbosity: 2, version: "DEV", resourcesTestFile: "./test-data/resources.json", @@ -52,22 +57,25 @@ func TestAzRBACCommand(t *testing.T) { roleDefinitionsTestFile: "./test-data/role-definitions.json", roleAssignmentsTestFile: "./test-data/role-assignments.json", wrapTableOutput: false, + azMergedTable: false, }, { name: "./cloudfox azure rbac", azOutputFormat: "all", azVerbosity: 2, + azOutputDirectory: "~/.cloudfox", version: "DEV", resourcesTestFile: "./test-data/resources.json", usersTestFile: "./test-data/users.json", roleDefinitionsTestFile: "./test-data/role-definitions.json", roleAssignmentsTestFile: "./test-data/role-assignments.json", wrapTableOutput: false, + azMergedTable: false, }, } internal.MockFileSystem(true) // Mocked functions to simulate Azure calls and responses - getSubscriptions = mockedGetSubscriptions + GetSubscriptions = mockedGetSubscriptions getAzureADUsers = mockedGetAzureADUsers getRoleDefinitions = mockedGetRoleDefinitions getRoleAssignments = mockedGetRoleAssignments @@ -82,7 +90,7 @@ func TestAzRBACCommand(t *testing.T) { globals.ROLE_DEFINITIONS_TEST_FILE = s.roleDefinitionsTestFile globals.ROLE_ASSIGNMENTS_TEST_FILE = s.roleAssignmentsTestFile - if err := AzRBACCommand(s.azTenantID, s.azSubscriptionID, s.azOutputFormat, s.version, s.azVerbosity, s.wrapTableOutput); err != nil { + if err := AzRBACCommand(s.azTenantID, s.azSubscriptionID, s.azOutputFormat, s.azOutputDirectory, s.version, 2, s.wrapTableOutput, s.azMergedTable); err != nil { fmt.Println(err) } } diff --git a/azure/shared.go b/azure/shared.go new file mode 100644 index 0000000..24a17e6 --- /dev/null +++ b/azure/shared.go @@ -0,0 +1,129 @@ +package azure + +import ( + "context" + "fmt" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions" + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/smithy-go/ptr" +) + +type TenantInfo struct { + ID *string + DefaultDomain *string + Subscriptions []SubsriptionInfo +} + +type SubsriptionInfo struct { + Subscription subscriptions.Subscription + ID string + Name string +} + +// function that takes a subscription ID and returns the DisplayName of the subscription +func GetSubscriptionNameFromID(subscriptionID string) *string { + subs := GetSubscriptions() + for _, s := range subs { + if ptr.ToString(s.SubscriptionID) == subscriptionID { + return s.DisplayName + } + } + return nil +} + +func GetSubscriptionIDFromName(subscriptionName string) *string { + subs := GetSubscriptions() + for _, s := range subs { + if ptr.ToString(s.DisplayName) == subscriptionName { + return s.SubscriptionID + } + } + return nil +} + +// function that takes the AzSubscription string and first checks to see if it is a valid subscription ID, and if not, checks to see if it is a valid subscription display name. It then returns the subscription ID +func GetSubscriptionID(subscription string) *string { + subs := GetSubscriptions() + for _, s := range subs { + if ptr.ToString(s.SubscriptionID) == subscription { + return s.SubscriptionID + } + if ptr.ToString(s.DisplayName) == subscription { + return s.SubscriptionID + } + } + return nil +} + +func GetSubscriptionsPerTenantID(tenantID string) []subscriptions.Subscription { + subs := GetSubscriptions() + var results []subscriptions.Subscription + for _, s := range subs { + if ptr.ToString(s.TenantID) == tenantID { + results = append(results, s) + } + } + return results +} + +func GetTenantIDPerSubscription(subscriptionID string) *string { + subs := GetSubscriptions() + for _, s := range subs { + if ptr.ToString(s.SubscriptionID) == subscriptionID { + return s.TenantID + } + if ptr.ToString(s.DisplayName) == subscriptionID { + return s.TenantID + } + } + return nil +} + +// function that determines if AzSubsriptionType is a subscription ID or a subscription display name and returns the AzSubsriptionType struct with both populated +func PopulateSubsriptionType(subscription string) SubsriptionInfo { + subs := GetSubscriptions() + for _, s := range subs { + if ptr.ToString(s.SubscriptionID) == subscription { + return SubsriptionInfo{ID: subscription, Name: ptr.ToString(s.DisplayName)} + } + if ptr.ToString(s.DisplayName) == subscription { + return SubsriptionInfo{ID: ptr.ToString(s.SubscriptionID), Name: subscription} + } + } + return SubsriptionInfo{} +} + +func GetDefaultDomainFromTenantID(tenantID string) (string, error) { + // Get the client using the function + client := internal.GetgraphRbacClient(tenantID) + + // List domains + domainList, err := client.List(context.Background(), "") + if err != nil { + return "", err + } + + for _, domain := range *domainList.Value { + if *domain.IsDefault { + primaryDomain := *domain.Name + return primaryDomain, nil + } + } + + return "", fmt.Errorf("No default domain found") +} + +func populateTenant(tenantID string) TenantInfo { + + for _, t := range getTenants() { + if ptr.ToString(t.TenantID) == tenantID || ptr.ToString(t.DefaultDomain) == tenantID { + var subscriptions []SubsriptionInfo + for _, s := range GetSubscriptionsPerTenantID(ptr.ToString(t.ID)) { + subscriptions = append(subscriptions, SubsriptionInfo{Subscription: s, ID: ptr.ToString(s.SubscriptionID), Name: ptr.ToString(s.DisplayName)}) + } + return TenantInfo{ID: t.TenantID, DefaultDomain: t.DefaultDomain, Subscriptions: subscriptions} + } + } + return TenantInfo{} +} diff --git a/azure/storage-accounts.go b/azure/storage.go similarity index 62% rename from azure/storage-accounts.go rename to azure/storage.go index 9f6140f..3febe71 100644 --- a/azure/storage-accounts.go +++ b/azure/storage.go @@ -22,61 +22,127 @@ import ( // 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 +func AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error { + var publicBlobURLs []string - var outputDirectory, controlMessagePrefix string - - if AzTenantID != "" && AzSubscriptionID == "" { - // ./cloudfox azure storage --tenant TENANT_ID - fmt.Printf( - "[%s][%s] Enumerating storage accounts for tenant %s\n", - color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), - color.CyanString(globals.AZ_STORAGE_MODULE_NAME), - AzTenantID) - header, body, publicBlobURLs, err = getStorageInfoPerTenant(AzTenantID) - controlMessagePrefix = fmt.Sprintf("tenant-%s", AzTenantID) - outputDirectory = filepath.Join( - globals.CLOUDFOX_BASE_DIRECTORY, - globals.AZ_DIR_BASE, - "tenants", - AzTenantID) - - } else if AzTenantID == "" && AzSubscriptionID != "" { - // ./cloudfox azure storage --subscription SUBSCRIPTION_ID - fmt.Printf( - "[%s][%s] Enumerating storage accounts for subscription %s\n", - color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), - color.CyanString(globals.AZ_STORAGE_MODULE_NAME), - AzSubscriptionID) - AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscriptionID)) - header, body, publicBlobURLs, err = getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID) - controlMessagePrefix = fmt.Sprintf("subscription-%s", AzSubscriptionID) - outputDirectory = filepath.Join( - globals.CLOUDFOX_BASE_DIRECTORY, - globals.AZ_DIR_BASE, - "subscriptions", - AzSubscriptionID) + + if AzTenantID != "" && AzSubscription == "" { + // cloudfox azure storage --tenant [TENANT_ID | PRIMARY_DOMAIN] + tenantInfo := populateTenant(AzTenantID) + + if AzMergedTable { + + // set up table vars + var header []string + var body [][]string + // setup logging client + o := internal.OutputClient{ + Verbosity: AzVerbosity, + CallingModule: globals.AZ_STORAGE_MODULE_NAME, + Table: internal.TableClient{ + Wrap: AzWrapTable, + }, + } + + var err error + + fmt.Printf("[%s][%s] Enumerating storage accounts for tenant %s\n", + color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME), + fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID))) + + o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain) + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level") + + header, body, publicBlobURLs, err = getStorageInfoPerTenant(ptr.ToString(tenantInfo.ID)) + + if err != nil { + return err + } + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(globals.AZ_STORAGE_MODULE_NAME)}) + + if body != nil { + o.WriteFullOutput(o.Table.TableFiles, nil) + } + if publicBlobURLs != nil { + err := writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, o.PrefixIdentifier, o.Table.DirectoryName, publicBlobURLs) + if err != nil { + return err + } + } + + } else { + + for _, s := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) { + runStorageCommandForSingleSubcription(ptr.ToString(s.SubscriptionID), AzOutputDirectory, AzVerbosity, AzWrapTable, Version) + } + } + + } else if AzTenantID == "" && AzSubscription != "" { + //cloudfox azure storage --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME] + runStorageCommandForSingleSubcription(AzSubscription, AzOutputDirectory, AzVerbosity, AzWrapTable, Version) + } else { // Error: please make a valid flag selection fmt.Println("Please enter a valid input with a valid flag. Use --help for info.") } + + return nil +} + +func runStorageCommandForSingleSubcription(AzSubscription string, AzOutputDirectory string, AzVerbosity int, AzWrapTable bool, Version string) error { + var err error + // setup logging client + o := internal.OutputClient{ + Verbosity: AzVerbosity, + CallingModule: globals.AZ_STORAGE_MODULE_NAME, + Table: internal.TableClient{ + Wrap: AzWrapTable, + }, + } + + // set up table vars + var header []string + var body [][]string + var publicBlobURLs []string + + tenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription)) + tenantInfo := populateTenant(tenantID) + AzSubscriptionInfo := PopulateSubsriptionType(AzSubscription) + o.PrefixIdentifier = AzSubscriptionInfo.Name + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name) + + fmt.Printf( + "[%s][%s] Enumerating storage accounts for subscription %s\n", + color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), + color.CyanString(globals.AZ_STORAGE_MODULE_NAME), + fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID)) + //AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription)) + header, body, publicBlobURLs, err = getStorageInfoPerSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID) if err != nil { return err } - fileNameWithoutExtension := globals.AZ_STORAGE_MODULE_NAME + + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(globals.AZ_STORAGE_MODULE_NAME)}) if body != nil { - internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_STORAGE_MODULE_NAME, AzWrapTable, controlMessagePrefix) + o.WriteFullOutput(o.Table.TableFiles, nil) + } if publicBlobURLs != nil { - err = writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, controlMessagePrefix, outputDirectory, publicBlobURLs) + err := writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, o.PrefixIdentifier, o.Table.DirectoryName, publicBlobURLs) if err != nil { return err } } return nil + } func writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory string, publicBlobURLs []string) error { @@ -128,7 +194,7 @@ func getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]strin var body [][]string var publicBlobURLs []string - for _, s := range getSubscriptions() { + for _, s := range GetSubscriptions() { if ptr.ToString(s.SubscriptionID) == AzSubscriptionID { header, body, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID)) if err != nil { @@ -140,7 +206,7 @@ func getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]strin } func getRelevantStorageAccountData(tenantID, subscriptionID string) ([]string, [][]string, []string, error) { - tableHeader := []string{"Subscription ID", "Storage Account Name", "Container Name", "Access Status"} + tableHeader := []string{"Subscription Name", "Storage Account Name", "Container Name", "Access Status"} var tableBody [][]string var publicBlobURLs []string storageAccounts, err := getStorageAccounts(subscriptionID) @@ -154,21 +220,33 @@ func getRelevantStorageAccountData(tenantID, subscriptionID string) ([]string, [ } containers, err := getStorageAccountContainers(blobClient) if err != nil { - return nil, nil, nil, err + // rather than return an error, we'll just add a row to the table highlighting the storage account name and that we couldn't get the containers + + tableBody = append(tableBody, + []string{ + subscriptionID, + ptr.ToString(sa.Name), + "Unknown", + "Authorization Failure"}) + + //return nil, nil, nil, nil } + for containerName, accessType := range containers { tableBody = append(tableBody, []string{ - subscriptionID, + ptr.ToString(GetSubscriptionNameFromID(subscriptionID)), ptr.ToString(sa.Name), containerName, accessType}) } urls, err := getPublicBlobURLs(blobClient, ptr.ToString(sa.Name), containers) - if err != nil { - return nil, nil, nil, err + if err == nil { + continue + //return nil, nil, nil, err } publicBlobURLs = append(publicBlobURLs, urls...) + } return tableHeader, tableBody, publicBlobURLs, nil } @@ -233,7 +311,8 @@ func getPublicBlobURLs(client *azblob.Client, storageAccountName string, contain if accessType == "public" { url, err := getPublicBlobURLsForContainer(client, storageAccountName, containerName) if err != nil { - return nil, err + //return nil, err + continue } publicBlobURLs = append(publicBlobURLs, url...) } diff --git a/azure/storage-accounts_test.go b/azure/storage_test.go similarity index 85% rename from azure/storage-accounts_test.go rename to azure/storage_test.go index cc6395e..eb16fcd 100644 --- a/azure/storage-accounts_test.go +++ b/azure/storage_test.go @@ -21,48 +21,56 @@ func TestAzStorageCommand(t *testing.T) { AzTenantID string AzSubscriptionID string AzOutputFormat string + azOutputDirectory string AzVerbosity int resourcesTestFile string storageAccountsTestFile string version string wrapTableOutput bool + azMergedTable bool }{ { name: "./cloudfox az storage --tenant 11111111-1111-1111-1111-11111111", AzTenantID: "11111111-1111-1111-1111-11111111", AzSubscriptionID: "", AzOutputFormat: "all", + azOutputDirectory: "~/.cloudfox", AzVerbosity: 2, resourcesTestFile: "./test-data/resources.json", storageAccountsTestFile: "./test-data/storage-accounts.json", version: "DEV", wrapTableOutput: false, + azMergedTable: false, }, { name: "./cloudfox az storage --subscription BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB", AzTenantID: "", AzSubscriptionID: "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBB", AzOutputFormat: "all", + azOutputDirectory: "~/.cloudfox", AzVerbosity: 2, resourcesTestFile: "./test-data/resources.json", storageAccountsTestFile: "./test-data/storage-accounts.json", version: "DEV", wrapTableOutput: false, + azMergedTable: false, }, { name: "./cloudfox az storage", AzOutputFormat: "all", + azOutputDirectory: "~/.cloudfox", AzVerbosity: 2, resourcesTestFile: "./test-data/resources.json", storageAccountsTestFile: "./test-data/storage-accounts.json", version: "DEV", wrapTableOutput: false, + azMergedTable: false, }, } internal.MockFileSystem(true) // Mocked functions to simulate Azure calls and responses getTenants = mockedGetTenants - getSubscriptions = mockedGetSubscriptions + GetSubscriptions = mockedGetSubscriptions getResourceGroups = mockedGetResourceGroups getStorageAccounts = mockedGetStorageAccounts @@ -72,7 +80,7 @@ func TestAzStorageCommand(t *testing.T) { globals.RESOURCES_TEST_FILE = s.resourcesTestFile globals.STORAGE_ACCOUNTS_TEST_FILE = s.storageAccountsTestFile - err := AzStorageCommand(s.AzTenantID, s.AzSubscriptionID, s.AzOutputFormat, s.version, s.AzVerbosity, s.wrapTableOutput) + err := AzStorageCommand(s.AzTenantID, s.AzSubscriptionID, s.AzOutputFormat, s.azOutputDirectory, s.version, s.AzVerbosity, s.wrapTableOutput, s.azMergedTable) if err != nil { log.Fatal(err) } diff --git a/azure/instances.go b/azure/vms.go similarity index 51% rename from azure/instances.go rename to azure/vms.go index 5dd4f14..1b4c54e 100644 --- a/azure/instances.go +++ b/azure/vms.go @@ -2,6 +2,7 @@ package azure import ( "context" + "encoding/base64" "encoding/json" "fmt" "os" @@ -19,86 +20,184 @@ import ( "github.com/kyokomi/emoji" ) -func AzInstancesCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, AzVerbosity int, AzWrapTable bool) error { - var header []string - var body [][]string - var outputDirectory, controlMessagePrefix string - - if AzTenantID != "" && AzSubscriptionID == "" { - // ./cloudfox azure instances --tenant TENANT_ID - fmt.Printf("[%s][%s] Enumerating VMs for tenant %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_INSTANCES_MODULE_NAME), AzTenantID) - controlMessagePrefix = fmt.Sprintf("tenant-%s", AzTenantID) - outputDirectory = filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, globals.AZ_DIR_TEN, AzTenantID) - header, body = getVMsPerTenantID(AzTenantID) - - } else if AzTenantID == "" && AzSubscriptionID != "" { - // ./cloudfox azure instances --subscription SUBSCRIPTION_ID - fmt.Printf("[%s][%s] Enumerating VMs for subscription %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_INSTANCES_MODULE_NAME), AzSubscriptionID) - controlMessagePrefix = fmt.Sprintf("subscription-%s", AzSubscriptionID) - outputDirectory = filepath.Join(globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, globals.AZ_DIR_SUB, AzSubscriptionID) - header, body = getVMsPerSubscriptionID(AzSubscriptionID) +func AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error { + + if AzTenantID != "" && AzSubscription == "" { + // cloudfox azure vms --tenant [TENANT_ID | PRIMARY_DOMAIN] + tenantInfo := populateTenant(AzTenantID) + + if AzMergedTable { + // set up table vars + var header []string + var body [][]string + var userData string + + o := internal.OutputClient{ + Verbosity: AzVerbosity, + CallingModule: globals.AZ_VMS_MODULE_NAME, + Table: internal.TableClient{ + Wrap: AzWrapTable, + }, + } + fmt.Printf("[%s][%s] Enumerating VMs for tenant %s\n", + color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_VMS_MODULE_NAME), + fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID))) + + o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain) + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level") + + // populate the table data + header, body, userData = getVMsPerTenantID(ptr.ToString(tenantInfo.ID)) + + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(globals.AZ_VMS_MODULE_NAME)}) + + if body != nil { + if userData != "" { + o.Loot.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "loot") + o.Loot.LootFiles = append(o.Loot.LootFiles, + internal.LootFile{ + Contents: userData, + Name: "virtualmachines-user-data"}) + o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles) + fmt.Println() + } else { + + o.WriteFullOutput(o.Table.TableFiles, nil) + fmt.Println() + } + + } + } else { + + for _, s := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) { + runVMsCommandForSingleSubscription(ptr.ToString(s.SubscriptionID), AzOutputDirectory, AzVerbosity, AzWrapTable, Version) + } + } + + } else if AzTenantID == "" && AzSubscription != "" { + // cloudfox azure vms --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME] + runVMsCommandForSingleSubscription(AzSubscription, AzOutputDirectory, AzVerbosity, AzWrapTable, Version) } else { // Error: please make a valid flag selection fmt.Println("Please enter a valid input with a valid flag. Use --help for info.") } - fileNameWithoutExtension := globals.AZ_INSTANCES_MODULE_NAME + return nil +} + +func runVMsCommandForSingleSubscription(AzSubscription string, AzOutputDirectory string, AzVerbosity int, AzWrapTable bool, Version string) error { + // set up table vars + var header []string + var body [][]string + var userData string + + o := internal.OutputClient{ + Verbosity: AzVerbosity, + CallingModule: globals.AZ_VMS_MODULE_NAME, + Table: internal.TableClient{ + Wrap: AzWrapTable, + }, + } + var AzSubscriptionInfo SubsriptionInfo + tenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription)) + tenantInfo := populateTenant(tenantID) + AzSubscriptionInfo = PopulateSubsriptionType(AzSubscription) + o.PrefixIdentifier = AzSubscriptionInfo.Name + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name) + + fmt.Printf("[%s][%s] Enumerating VMs for subscription %s\n", + color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_VMS_MODULE_NAME), + fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID)) + + // populate the table data + header, body, userData = getVMsPerSubscriptionID(AzSubscriptionInfo.ID) + + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(globals.AZ_VMS_MODULE_NAME)}) + if body != nil { - internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_INSTANCES_MODULE_NAME, AzWrapTable, controlMessagePrefix) + if userData != "" { + o.Loot.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name, "loot") + o.Loot.LootFiles = append(o.Loot.LootFiles, + internal.LootFile{ + Contents: userData, + Name: "virtualmachines-user-data"}) + o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles) + fmt.Println() + } else { + + o.WriteFullOutput(o.Table.TableFiles, nil) + fmt.Println() + } + } return nil } -func getVMsPerTenantID(AzTenantID string) ([]string, [][]string) { +func getVMsPerTenantID(AzTenantID string) ([]string, [][]string, string) { var resultsHeader []string var resultsBody, b [][]string + var userDataCombined, userData string var err error for _, s := range GetSubscriptionsPerTenantID(AzTenantID) { for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) { - resultsHeader, b, err = getComputeRelevantData(s, rg) + resultsHeader, b, userData, err = getComputeRelevantData(s, rg) if err != nil { - fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_INSTANCES_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID)) + fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_VMS_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID)) } else { resultsBody = append(resultsBody, b...) + userDataCombined += userData } + } } - return resultsHeader, resultsBody + return resultsHeader, resultsBody, userDataCombined } -func getVMsPerSubscriptionID(AzSubscriptionID string) ([]string, [][]string) { +func getVMsPerSubscriptionID(AzSubscriptionID string) ([]string, [][]string, string) { var resultsHeader []string var resultsBody, b [][]string + var userDataCombined, userData string var err error - for _, s := range getSubscriptions() { + for _, s := range GetSubscriptions() { if ptr.ToString(s.SubscriptionID) == AzSubscriptionID { for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) { - resultsHeader, b, err = getComputeRelevantData(s, rg) + resultsHeader, b, userData, err = getComputeRelevantData(s, rg) if err != nil { - fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_INSTANCES_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID)) + fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_VMS_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID)) } else { resultsBody = append(resultsBody, b...) + userDataCombined += userData } } } } - return resultsHeader, resultsBody + return resultsHeader, resultsBody, userDataCombined } -func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group) ([]string, [][]string, error) { - header := []string{"Subscription ID", "VM Name", "VM Location", "Private IPs", "Public IPs", "Admin Username", "Resource Group Name"} +func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group) ([]string, [][]string, string, error) { + header := []string{"Subscription Name", "VM Name", "VM Location", "Private IPs", "Public IPs", "Admin Username", "Resource Group Name"} var body [][]string + var userDataString string subscriptionID := ptr.ToString(sub.SubscriptionID) + subscriptionName := ptr.ToString(sub.DisplayName) resourceGroupName := ptr.ToString(rg.Name) vms, err := getComputeVMsPerResourceGroup(subscriptionID, resourceGroupName) if err != nil { - return nil, nil, fmt.Errorf("error fetching vms for resource group %s: %s", resourceGroupName, err) + return nil, nil, "", fmt.Errorf("error fetching vms for resource group %s: %s", resourceGroupName, err) } for _, vm := range vms { @@ -107,11 +206,38 @@ func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group) adminUsername = ptr.ToString(vm.OsProfile.AdminUsername) } privateIPs, publicIPs := getIPs(ptr.ToString(sub.SubscriptionID), ptr.ToString(rg.Name), vm) + // get userdata + vmDetails, err := getComputeVmInfo(subscriptionID, resourceGroupName, ptr.ToString(vm.Name)) + if err != nil { + fmt.Println("error fetching vm details for vm: ", ptr.ToString(vm.Name)) + } + + if vmDetails.VirtualMachineProperties != nil && vmDetails.VirtualMachineProperties.UserData != nil { + userData, err := base64.StdEncoding.DecodeString(ptr.ToString(vmDetails.VirtualMachineProperties.UserData)) + if err != nil { + fmt.Println("error decoding userdata for vm: ", ptr.ToString(vm.Name)) + } + //append userdata from this vm to the string with headers and newlines for VM name, location, and resource group name + userDataString += fmt.Sprintf( + "===============================================================\n"+ + "VM Name: %s\n"+ + "Subscription Name: %s\n"+ + "VM Location: %s\n"+ + "Resource Group Name: %s\n\n"+ + "UserData:\n%s\n\n", + ptr.ToString(vm.Name), + ptr.ToString(sub.DisplayName), + ptr.ToString(vmDetails.Location), + ptr.ToString(rg.Name), + string(userData), + ) + + } body = append( body, []string{ - subscriptionID, + subscriptionName, ptr.ToString(vm.Name), ptr.ToString(vm.Location), strings.Join(privateIPs, "\n"), @@ -121,7 +247,7 @@ func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group) }, ) } - return header, body, nil + return header, body, userDataString, nil } var getComputeVMsPerResourceGroup = getComputeVMsPerResourceGroupOriginal @@ -142,6 +268,16 @@ func getComputeVMsPerResourceGroupOriginal(subscriptionID string, resourceGroup return vms, nil } +// get vms with user-data view +func getComputeVmInfo(subscriptionID string, resourceGroup string, vmName string) (compute.VirtualMachine, error) { + computeClient := internal.GetVirtualMachinesClient(subscriptionID) + vm, err := computeClient.Get(context.Background(), resourceGroup, vmName, compute.InstanceViewTypesUserData) + if err != nil { + return compute.VirtualMachine{}, fmt.Errorf("could not get vm %s. %s", vmName, err) + } + return vm, nil +} + func mockedGetComputeVMsPerResourceGroup(subscriptionID, resourceGroup string) ([]compute.VirtualMachine, error) { testFile, err := os.ReadFile(globals.VMS_TEST_FILE) if err != nil { diff --git a/azure/instances_test.go b/azure/vms_test.go similarity index 74% rename from azure/instances_test.go rename to azure/vms_test.go index 699a73e..3733926 100644 --- a/azure/instances_test.go +++ b/azure/vms_test.go @@ -9,9 +9,9 @@ import ( "github.com/BishopFox/cloudfox/internal" ) -func TestAzInstancesCommand(t *testing.T) { +func TestAzVMsCommand(t *testing.T) { fmt.Println() - fmt.Println("[test case] Azure Instances Command") + fmt.Println("[test case] Azure vms Command") // Test case parameters internal.MockFileSystem(true) @@ -21,18 +21,21 @@ func TestAzInstancesCommand(t *testing.T) { azSubscriptionID string azVerbosity int azOutputFormat string + azOutputDirectory string version string resourcesTestFile string vmsTestFile string nicsTestFile string publicIPsTestFile string wrapTableOutput bool + azMergedTable bool }{ { - name: "./cloudfox azure instances --tenant 11111111-1111-1111-1111-11111111", + name: "./cloudfox azure vms --tenant 11111111-1111-1111-1111-11111111", azTenantID: "11111111-1111-1111-1111-11111111", azSubscriptionID: "", azVerbosity: 2, + azOutputDirectory: "~/.cloudfox", azOutputFormat: "all", version: "DEV", resourcesTestFile: "./test-data/resources.json", @@ -40,12 +43,14 @@ func TestAzInstancesCommand(t *testing.T) { nicsTestFile: "./test-data/nics.json", publicIPsTestFile: "./test-data/public-ips.json", wrapTableOutput: false, + azMergedTable: false, }, { - name: "./cloudfox azure instances --subscription AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA", + name: "./cloudfox azure vms --subscription AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA", azTenantID: "", azSubscriptionID: "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA", azVerbosity: 2, + azOutputDirectory: "~/.cloudfox", azOutputFormat: "all", version: "DEV", resourcesTestFile: "./test-data/resources.json", @@ -53,22 +58,25 @@ func TestAzInstancesCommand(t *testing.T) { nicsTestFile: "./test-data/nics.json", publicIPsTestFile: "./test-data/public-ips.json", wrapTableOutput: false, + azMergedTable: false, }, { - name: "./cloudfox azure instances", + name: "./cloudfox azure vms", azVerbosity: 2, azOutputFormat: "all", + azOutputDirectory: "~/.cloudfox", version: "DEV", resourcesTestFile: "./test-data/resources.json", vmsTestFile: "./test-data/vms.json", nicsTestFile: "./test-data/nics.json", publicIPsTestFile: "./test-data/public-ips.json", wrapTableOutput: false, + azMergedTable: false, }, } // Mocked functions to simulate Azure calls and responses - getSubscriptions = mockedGetSubscriptions + GetSubscriptions = mockedGetSubscriptions getResourceGroups = mockedGetResourceGroups getComputeVMsPerResourceGroup = mockedGetComputeVMsPerResourceGroup getNICdetails = mockedGetNICdetails @@ -82,7 +90,7 @@ func TestAzInstancesCommand(t *testing.T) { globals.NICS_TEST_FILE = s.nicsTestFile globals.PUBLIC_IPS_TEST_FILE = s.publicIPsTestFile - err := AzInstancesCommand(s.azTenantID, s.azSubscriptionID, s.azOutputFormat, s.version, 2, s.wrapTableOutput) + err := AzVMsCommand(s.azTenantID, s.azSubscriptionID, s.azOutputFormat, s.azOutputDirectory, s.version, 2, s.wrapTableOutput, s.azMergedTable) if err != nil { log.Fatalf(err.Error()) } diff --git a/azure/whoami.go b/azure/whoami.go index b063080..9e9c8f3 100644 --- a/azure/whoami.go +++ b/azure/whoami.go @@ -6,6 +6,9 @@ import ( "fmt" "log" "os" + "path/filepath" + "strconv" + "time" "github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources" "github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions" @@ -16,32 +19,65 @@ import ( "github.com/kyokomi/emoji" ) -func AzWhoamiCommand(version string, AzWrapTable bool) error { +func AzWhoamiCommand(AzOutputDirectory, version string, AzWrapTable bool, AzVerbosity int, AzWhoamiListRGsAlso bool) error { + o := internal.OutputClient{ + Verbosity: AzVerbosity, + CallingModule: globals.AZ_WHOAMI_MODULE_NAME, + Table: internal.TableClient{ + Wrap: AzWrapTable, + }, + } + fmt.Printf("[%s][%s] Enumerating Azure CLI sessions...\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", version)), color.CyanString(globals.AZ_WHOAMI_MODULE_NAME)) var header []string var body [][]string - header, body = getWhoamiRelevantDataPerRG() - internal.PrintTableToScreen(header, body, AzWrapTable) + o.PrefixIdentifier = "N/A" + if !AzWhoamiListRGsAlso { + // cloudfox azure whoami + header, body = getWhoamiRelevantDataSubsOnly() + o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "whoami-data") + // append timestamp to filename (time from epoch) + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: fmt.Sprintf(globals.AZ_WHOAMI_MODULE_NAME+"-subs-only") + "-" + strconv.FormatInt((time.Now().Unix()), 10)}) + + } else { + // cloudfox azure whoami --list-rgs + header, body = getWhoamiRelevantDataPerRG() + o.Table.DirectoryName = filepath.Join(ptr.ToString(internal.GetLogDirPath()), globals.AZ_DIR_BASE, "whoami-data") + o.Table.TableFiles = append(o.Table.TableFiles, + internal.TableFile{ + Header: header, + Body: body, + Name: globals.AZ_WHOAMI_MODULE_NAME + "-" + strconv.FormatInt((time.Now().Unix()), 10)}) + } + //internal.PrintTableToScreen(header, body, AzWrapTable) + + o.WriteFullOutput(o.Table.TableFiles, nil) + return nil } func getWhoamiRelevantDataPerRG() ([]string, [][]string) { - tableHead := []string{"Tenant ID", "Subscription ID", "Subscription Name", "RG Name", "Region", "Domain"} + tableHead := []string{"Tenant ID", "Tentant Primary Domain", "Subscription ID", "Subscription Name", "RG Name", "Region"} var tableBody [][]string for _, t := range getTenants() { - for _, s := range getSubscriptions() { + for _, s := range GetSubscriptions() { if ptr.ToString(t.TenantID) == ptr.ToString(s.TenantID) { for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) { tableBody = append( tableBody, []string{ ptr.ToString(s.TenantID), + ptr.ToString(t.DefaultDomain), ptr.ToString(s.SubscriptionID), ptr.ToString(s.DisplayName), ptr.ToString(rg.Name), ptr.ToString(rg.Location), - ptr.ToString(t.DefaultDomain)}) + }) } } } @@ -50,25 +86,26 @@ func getWhoamiRelevantDataPerRG() ([]string, [][]string) { return tableHead, tableBody } -func GetTenantIDPerSubscription(subscriptionID string) *string { - subs := getSubscriptions() - for _, s := range subs { - if ptr.ToString(s.SubscriptionID) == subscriptionID { - return s.TenantID - } - } - return nil -} +func getWhoamiRelevantDataSubsOnly() ([]string, [][]string) { + tableHead := []string{"Tenant ID", "Tenant Primary Domain", "Subscription ID", "Subscription Name"} + var tableBody [][]string -func GetSubscriptionsPerTenantID(tenantID string) []subscriptions.Subscription { - subs := getSubscriptions() - var results []subscriptions.Subscription - for _, s := range subs { - if ptr.ToString(s.TenantID) == tenantID { - results = append(results, s) + for _, t := range getTenants() { + for _, s := range GetSubscriptions() { + if ptr.ToString(t.TenantID) == ptr.ToString(s.TenantID) { + tableBody = append( + tableBody, + []string{ + ptr.ToString(s.TenantID), + ptr.ToString(t.DefaultDomain), + ptr.ToString(s.SubscriptionID), + ptr.ToString(s.DisplayName), + }) + } } } - return results + + return tableHead, tableBody } var getTenants = getTenantsOriginal @@ -97,7 +134,7 @@ func mockedGetTenants() []subscriptions.TenantIDDescription { return results } -var getSubscriptions = getSubscriptionsOriginal +var GetSubscriptions = getSubscriptionsOriginal func getSubscriptionsOriginal() []subscriptions.Subscription { var results []subscriptions.Subscription diff --git a/azure/whoami_test.go b/azure/whoami_test.go index 4abea23..4395cf6 100644 --- a/azure/whoami_test.go +++ b/azure/whoami_test.go @@ -15,7 +15,7 @@ func TestAzWhoamiCommand(t *testing.T) { // Mocked functions to simulate Azure calls and responses getTenants = mockedGetTenants - getSubscriptions = mockedGetSubscriptions + GetSubscriptions = mockedGetSubscriptions getResourceGroups = mockedGetResourceGroups // Test case parameters @@ -26,6 +26,7 @@ func TestAzWhoamiCommand(t *testing.T) { azExtendedFilter bool version string wrapTableOutput bool + azOutputDirectory string }{ { name: "./cloudfox azure whoami", @@ -33,6 +34,7 @@ func TestAzWhoamiCommand(t *testing.T) { azExtendedFilter: false, version: "DEV", wrapTableOutput: false, + azOutputDirectory: "~/.cloudfox", }, { name: "./cloudfox azure whoami --extended", @@ -40,13 +42,14 @@ func TestAzWhoamiCommand(t *testing.T) { azExtendedFilter: true, version: "DEV", wrapTableOutput: true, + azOutputDirectory: "~/.cloudfox", }, } for _, s := range subtests { globals.RESOURCES_TEST_FILE = s.resourcesTestFile fmt.Println() fmt.Printf("[subtest] %s\n", s.name) - AzWhoamiCommand(s.version, s.wrapTableOutput) + AzWhoamiCommand(s.azOutputDirectory, s.version, s.wrapTableOutput, 1, false) } } diff --git a/cli/aws.go b/cli/aws.go index a399f80..0a0ee55 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -13,10 +13,16 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigatewayv2" "github.com/aws/aws-sdk-go-v2/service/apprunner" + "github.com/aws/aws-sdk-go-v2/service/athena" + "github.com/aws/aws-sdk-go-v2/service/cloud9" "github.com/aws/aws-sdk-go-v2/service/cloudformation" "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudtrail" + "github.com/aws/aws-sdk-go-v2/service/codeartifact" "github.com/aws/aws-sdk-go-v2/service/codebuild" + "github.com/aws/aws-sdk-go-v2/service/codecommit" + "github.com/aws/aws-sdk-go-v2/service/codedeploy" + "github.com/aws/aws-sdk-go-v2/service/datapipeline" "github.com/aws/aws-sdk-go-v2/service/docdb" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/ec2" @@ -25,12 +31,15 @@ import ( "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/elasticbeanstalk" "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/emr" "github.com/aws/aws-sdk-go-v2/service/fsx" "github.com/aws/aws-sdk-go-v2/service/glue" "github.com/aws/aws-sdk-go-v2/service/grafana" "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/kinesis" "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lightsail" "github.com/aws/aws-sdk-go-v2/service/mq" @@ -61,19 +70,22 @@ var ( red = color.New(color.FgRed).SprintFunc() defaultOutputDir = ptr.ToString(internal.GetLogDirPath()) - AWSProfile string - AWSProfilesList string - AWSAllProfiles bool - AWSProfiles []string - AWSConfirm bool - AWSOutputFormat string + AWSProfile string + AWSProfilesList string + AWSAllProfiles bool + AWSProfiles []string + AWSConfirm bool + AWSOutputType string + AWSTableCols string + AWSOutputDirectory string AWSSkipAdminCheck bool AWSWrapTable bool AWSUseCache bool - Goroutines int - Verbosity int - AWSCommands = &cobra.Command{ + + Goroutines int + Verbosity int + AWSCommands = &cobra.Command{ Use: "aws", Short: "See \"Available Commands\" for AWS Modules", Run: func(cmd *cobra.Command, args []string) { @@ -106,7 +118,8 @@ var ( PostRun: awsPostRun, } - BucketsCommand = &cobra.Command{ + CheckBucketPolicies bool + BucketsCommand = &cobra.Command{ Use: "buckets", Aliases: []string{"bucket"}, Short: "Enumerate all of the buckets. Get loot file with s3 commands to list/download bucket contents", @@ -210,7 +223,7 @@ var ( EnvsCommand = &cobra.Command{ Use: "env-vars", Aliases: []string{"envs", "envvars", "env"}, - Short: "Enumerate the environment variables from mutliple services that have them", + Short: "Enumerate the environment variables from multiple services that have them", Long: "\nUse case examples:\n" + os.Args[0] + " aws env-vars --profile readonly_profile", PreRun: awsPreRun, @@ -341,7 +354,10 @@ var ( Short: "Enumerate IAM permissions per principal", Long: "\nUse case examples:\n" + os.Args[0] + " aws permissions --profile profile\n" + - os.Args[0] + " aws permissions --profile profile --principal arn:aws:iam::111111111111:role/test123", + os.Args[0] + " aws permissions --profile profile --principal arn:aws:iam::111111111111:role/test123" + + "\n\nAvailable Column Names:\n" + + "Type, Name, Arn, Policy, Policy Name, Policy Arn, Effect, Action, Resource, Condition\n", + PreRun: awsPreRun, Run: runPermissionsCommand, PostRun: awsPostRun, @@ -592,13 +608,15 @@ func runAccessKeysCommand(cmd *cobra.Command, args []string) { continue } m := aws.AccessKeysModule{ - IAMClient: iam.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + IAMClient: iam.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintAccessKeys(AccessKeysFilter, AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintAccessKeys(AccessKeysFilter, AWSOutputDirectory, Verbosity) } } @@ -618,7 +636,7 @@ func runApiGwCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, WrapTable: AWSWrapTable, } - m.PrintApiGws(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintApiGws(AWSOutputDirectory, Verbosity) } } @@ -629,22 +647,18 @@ func runBucketsCommand(cmd *cobra.Command, args []string) { continue } - // cloudFoxS3Client := aws.CloudFoxS3Client{ - // S3Client: s3.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - // Caller: *caller, - // AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - // AWSProfile: profile, - // } - m := aws.BucketsModule{ - S3Client: s3.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + S3Client: s3.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + CheckBucketPolicies: CheckBucketPolicies, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintBuckets(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintBuckets(AWSOutputDirectory, Verbosity) } } @@ -663,8 +677,10 @@ func runCloudformationCommand(cmd *cobra.Command, args []string) { AWSProfile: profile, Goroutines: Goroutines, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintCloudformationStacks(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintCloudformationStacks(AWSOutputDirectory, Verbosity) } } @@ -683,8 +699,10 @@ func runCodeBuildCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintCodeBuildProjects(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintCodeBuildProjects(AWSOutputDirectory, Verbosity) } } @@ -706,8 +724,10 @@ func runDatabasesCommand(cmd *cobra.Command, args []string) { AWSProfile: profile, Goroutines: Goroutines, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintDatabases(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintDatabases(AWSOutputDirectory, Verbosity) } } @@ -719,14 +739,16 @@ func runECRCommand(cmd *cobra.Command, args []string) { continue } m := aws.ECRModule{ - ECRClient: ecr.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + ECRClient: ecr.NewFromConfig(AWSConfig), + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintECR(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintECR(AWSOutputDirectory, Verbosity) } } @@ -742,13 +764,15 @@ func runSQSCommand(cmd *cobra.Command, args []string) { StorePolicies: StoreSQSAccessPolicies, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintSQS(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintSQS(AWSOutputDirectory, Verbosity) } } @@ -759,7 +783,7 @@ func runSNSCommand(cmd *cobra.Command, args []string) { continue } cloudFoxSNSClient := aws.InitCloudFoxSNSClient(*caller, profile, cmd.Root().Version, Goroutines, AWSWrapTable) - cloudFoxSNSClient.PrintSNS(AWSOutputFormat, AWSOutputDirectory, Verbosity) + cloudFoxSNSClient.PrintSNS(AWSOutputDirectory, Verbosity) } } @@ -780,8 +804,10 @@ func runEKSCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.EKS(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.EKS(AWSOutputDirectory, Verbosity) } } @@ -809,13 +835,15 @@ func runEndpointsCommand(cmd *cobra.Command, args []string) { RedshiftClient: redshift.NewFromConfig(AWSConfig), S3Client: s3.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintEndpoints(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintEndpoints(AWSOutputDirectory, Verbosity) } } @@ -828,11 +856,13 @@ func runEnvsCommand(cmd *cobra.Command, args []string) { } m := aws.EnvsModule{ - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, ECSClient: ecs.NewFromConfig(AWSConfig), AppRunnerClient: apprunner.NewFromConfig(AWSConfig), @@ -840,7 +870,7 @@ func runEnvsCommand(cmd *cobra.Command, args []string) { LightsailClient: lightsail.NewFromConfig(AWSConfig), SagemakerClient: sagemaker.NewFromConfig(AWSConfig), } - m.PrintEnvs(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintEnvs(AWSOutputDirectory, Verbosity) } } @@ -855,13 +885,15 @@ func runFilesystemsCommand(cmd *cobra.Command, args []string) { EFSClient: efs.NewFromConfig(AWSConfig), FSxClient: fsx.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - WrapTable: AWSWrapTable, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - filesystems.PrintFilesystems(AWSOutputFormat, AWSOutputDirectory, Verbosity) + filesystems.PrintFilesystems(AWSOutputDirectory, Verbosity) } } @@ -873,13 +905,15 @@ func runIamSimulatorCommand(cmd *cobra.Command, args []string) { continue } m := aws.IamSimulatorModule{ - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputDirectory, Verbosity) } } @@ -900,8 +934,10 @@ func runInstancesCommand(cmd *cobra.Command, args []string) { AWSProfile: profile, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.Instances(InstancesFilter, AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.Instances(InstancesFilter, AWSOutputDirectory, Verbosity) } } @@ -913,41 +949,55 @@ func runInventoryCommand(cmd *cobra.Command, args []string) { continue } m := aws.Inventory2Module{ - APIGatewayClient: apigateway.NewFromConfig(AWSConfig), - APIGatewayv2Client: apigatewayv2.NewFromConfig(AWSConfig), - AppRunnerClient: apprunner.NewFromConfig(AWSConfig), - CloudFormationClient: cloudformation.NewFromConfig(AWSConfig), - CloudfrontClient: cloudfront.NewFromConfig(AWSConfig), - CodeBuildClient: codebuild.NewFromConfig(AWSConfig), - DynamoDBClient: dynamodb.NewFromConfig(AWSConfig), - EC2Client: ec2.NewFromConfig(AWSConfig), - ECSClient: ecs.NewFromConfig(AWSConfig), - EKSClient: eks.NewFromConfig(AWSConfig), - ELBClient: elasticloadbalancing.NewFromConfig(AWSConfig), - ELBv2Client: elasticloadbalancingv2.NewFromConfig(AWSConfig), - GlueClient: glue.NewFromConfig(AWSConfig), - GrafanaClient: grafana.NewFromConfig(AWSConfig), - IAMClient: iam.NewFromConfig(AWSConfig), - LambdaClient: lambda.NewFromConfig(AWSConfig), - LightsailClient: lightsail.NewFromConfig(AWSConfig), - MQClient: mq.NewFromConfig(AWSConfig), - OpenSearchClient: opensearch.NewFromConfig(AWSConfig), - RDSClient: rds.NewFromConfig(AWSConfig), - RedshiftClient: redshift.NewFromConfig(AWSConfig), - S3Client: s3.NewFromConfig(AWSConfig), - SecretsManagerClient: secretsmanager.NewFromConfig(AWSConfig), - SNSClient: sns.NewFromConfig(AWSConfig), - SQSClient: sqs.NewFromConfig(AWSConfig), - SSMClient: ssm.NewFromConfig(AWSConfig), - StepFunctionClient: sfn.NewFromConfig(AWSConfig), - - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - } - m.PrintInventoryPerRegion(AWSOutputFormat, AWSOutputDirectory, Verbosity) + APIGatewayClient: apigateway.NewFromConfig(AWSConfig), + APIGatewayv2Client: apigatewayv2.NewFromConfig(AWSConfig), + AppRunnerClient: apprunner.NewFromConfig(AWSConfig), + AthenaClient: athena.NewFromConfig(AWSConfig), + Cloud9Client: cloud9.NewFromConfig(AWSConfig), + CloudFormationClient: cloudformation.NewFromConfig(AWSConfig), + CloudfrontClient: cloudfront.NewFromConfig(AWSConfig), + CodeArtifactClient: codeartifact.NewFromConfig(AWSConfig), + CodeBuildClient: codebuild.NewFromConfig(AWSConfig), + CodeCommitClient: codecommit.NewFromConfig(AWSConfig), + CodeDeployClient: codedeploy.NewFromConfig(AWSConfig), + DataPipelineClient: datapipeline.NewFromConfig(AWSConfig), + DynamoDBClient: dynamodb.NewFromConfig(AWSConfig), + EC2Client: ec2.NewFromConfig(AWSConfig), + ECSClient: ecs.NewFromConfig(AWSConfig), + ECRClient: ecr.NewFromConfig(AWSConfig), + EKSClient: eks.NewFromConfig(AWSConfig), + ELBClient: elasticloadbalancing.NewFromConfig(AWSConfig), + ELBv2Client: elasticloadbalancingv2.NewFromConfig(AWSConfig), + ElasticacheClient: elasticache.NewFromConfig(AWSConfig), + ElasticBeanstalkClient: elasticbeanstalk.NewFromConfig(AWSConfig), + EMRClient: emr.NewFromConfig(AWSConfig), + GlueClient: glue.NewFromConfig(AWSConfig), + GrafanaClient: grafana.NewFromConfig(AWSConfig), + IAMClient: iam.NewFromConfig(AWSConfig), + KinesisClient: kinesis.NewFromConfig(AWSConfig), + LambdaClient: lambda.NewFromConfig(AWSConfig), + LightsailClient: lightsail.NewFromConfig(AWSConfig), + MQClient: mq.NewFromConfig(AWSConfig), + OpenSearchClient: opensearch.NewFromConfig(AWSConfig), + RDSClient: rds.NewFromConfig(AWSConfig), + RedshiftClient: redshift.NewFromConfig(AWSConfig), + Route53Client: route53.NewFromConfig(AWSConfig), + S3Client: s3.NewFromConfig(AWSConfig), + SecretsManagerClient: secretsmanager.NewFromConfig(AWSConfig), + SNSClient: sns.NewFromConfig(AWSConfig), + SQSClient: sqs.NewFromConfig(AWSConfig), + SSMClient: ssm.NewFromConfig(AWSConfig), + StepFunctionClient: sfn.NewFromConfig(AWSConfig), + + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + } + m.PrintInventoryPerRegion(AWSOutputDirectory, Verbosity) } } @@ -967,8 +1017,10 @@ func runLambdasCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintLambdas(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintLambdas(AWSOutputDirectory, Verbosity) } } @@ -982,13 +1034,15 @@ func runOutboundAssumedRolesCommand(cmd *cobra.Command, args []string) { m := aws.OutboundAssumedRolesModule{ CloudTrailClient: cloudtrail.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintOutboundRoleTrusts(OutboundAssumedRolesDays, AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintOutboundRoleTrusts(OutboundAssumedRolesDays, AWSOutputDirectory, Verbosity) } } @@ -1004,8 +1058,10 @@ func runOrgsCommand(cmd *cobra.Command, args []string) { Caller: *caller, AWSProfile: profile, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintOrgAccounts(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintOrgAccounts(AWSOutputDirectory, Verbosity) } } @@ -1017,13 +1073,15 @@ func runPermissionsCommand(cmd *cobra.Command, args []string) { continue } m := aws.IamPermissionsModule{ - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSTableCols: AWSTableCols, + AWSOutputType: AWSOutputType, } - m.PrintIamPermissions(AWSOutputFormat, AWSOutputDirectory, Verbosity, PermissionsPrincipal) + m.PrintIamPermissions(AWSOutputDirectory, Verbosity, PermissionsPrincipal) } } @@ -1034,12 +1092,14 @@ func runPmapperCommand(cmd *cobra.Command, args []string) { continue } m := aws.PmapperModule{ - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintPmapperData(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintPmapperData(AWSOutputDirectory, Verbosity) } } @@ -1051,13 +1111,15 @@ func runPrincipalsCommand(cmd *cobra.Command, args []string) { continue } m := aws.IamPrincipalsModule{ - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintIamPrincipals(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintIamPrincipals(AWSOutputDirectory, Verbosity) } } @@ -1069,14 +1131,16 @@ func runRAMCommand(cmd *cobra.Command, args []string) { continue } ram := aws.RAMModule{ - RAMClient: ram.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - WrapTable: AWSWrapTable, + RAMClient: ram.NewFromConfig(AWSConfig), + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - ram.PrintRAM(AWSOutputFormat, AWSOutputDirectory, Verbosity) + ram.PrintRAM(AWSOutputDirectory, Verbosity) } } @@ -1094,8 +1158,10 @@ func runResourceTrustsCommand(cmd *cobra.Command, args []string) { AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), WrapTable: AWSWrapTable, CloudFoxVersion: cmd.Root().Version, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintResources(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintResources(AWSOutputDirectory, Verbosity) } } @@ -1113,8 +1179,10 @@ func runRoleTrustCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintRoleTrusts(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintRoleTrusts(AWSOutputDirectory, Verbosity) } } @@ -1128,13 +1196,15 @@ func runRoute53Command(cmd *cobra.Command, args []string) { m := aws.Route53Module{ Route53Client: route53.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintRoute53(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintRoute53(AWSOutputDirectory, Verbosity) } } @@ -1149,13 +1219,15 @@ func runSecretsCommand(cmd *cobra.Command, args []string) { SecretsManagerClient: secretsmanager.NewFromConfig(AWSConfig), SSMClient: ssm.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintSecrets(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintSecrets(AWSOutputDirectory, Verbosity) } } @@ -1174,8 +1246,10 @@ func runTagsCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, WrapTable: AWSWrapTable, MaxResourcesPerRegion: MaxResourcesPerRegion, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintTags(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.PrintTags(AWSOutputDirectory, Verbosity) } } @@ -1186,12 +1260,9 @@ func runECSTasksCommand(cmd *cobra.Command, args []string) { continue } m := aws.ECSTasksModule{ - DescribeTasksClient: ecs.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - DescribeTaskDefinitionClient: ecs.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - ListTasksClient: ecs.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - ListClustersClient: ecs.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - DescribeNetworkInterfacesClient: ec2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - IAMClient: iam.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), + EC2Client: ec2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), + ECSClient: ecs.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), + IAMClient: iam.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), Caller: *caller, AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), @@ -1199,8 +1270,10 @@ func runECSTasksCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.ECSTasks(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.ECSTasks(AWSOutputDirectory, Verbosity) } } @@ -1212,14 +1285,16 @@ func runENICommand(cmd *cobra.Command, args []string) { } m := aws.ElasticNetworkInterfacesModule{ //EC2Client: ec2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - DescribeNetworkInterfacesClient: ec2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), + EC2Client: ec2.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.ElasticNetworkInterfaces(AWSOutputFormat, AWSOutputDirectory, Verbosity) + m.ElasticNetworkInterfaces(AWSOutputDirectory, Verbosity) } } @@ -1243,8 +1318,10 @@ func runNetworkPortsCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, WrapTable: AWSWrapTable, Verbosity: Verbosity, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - m.PrintNetworkPorts(AWSOutputFormat, AWSOutputDirectory) + m.PrintNetworkPorts(AWSOutputDirectory) } } @@ -1259,9 +1336,15 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { apiGatewayClient := apigateway.NewFromConfig(AWSConfig) apiGatewayv2Client := apigatewayv2.NewFromConfig(AWSConfig) appRunnerClient := apprunner.NewFromConfig(AWSConfig) + athenaClient := athena.NewFromConfig(AWSConfig) + cloud9Client := cloud9.NewFromConfig(AWSConfig) cloudFormationClient := cloudformation.NewFromConfig(AWSConfig) cloudfrontClient := cloudfront.NewFromConfig(AWSConfig) + codeArtifactClient := codeartifact.NewFromConfig(AWSConfig) codeBuildClient := codebuild.NewFromConfig(AWSConfig) + codeCommitClient := codecommit.NewFromConfig(AWSConfig) + codeDeployClient := codedeploy.NewFromConfig(AWSConfig) + dataPipelineClient := datapipeline.NewFromConfig(AWSConfig) docdbClient := docdb.NewFromConfig(AWSConfig) dynamodbClient := dynamodb.NewFromConfig(AWSConfig) ec2Client := ec2.NewFromConfig(AWSConfig) @@ -1270,12 +1353,15 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { efsClient := efs.NewFromConfig(AWSConfig) eksClient := eks.NewFromConfig(AWSConfig) elasticacheClient := elasticache.NewFromConfig(AWSConfig) + elasticBeanstalkClient := elasticbeanstalk.NewFromConfig(AWSConfig) elbClient := elasticloadbalancing.NewFromConfig(AWSConfig) elbv2Client := elasticloadbalancingv2.NewFromConfig(AWSConfig) + emrClient := emr.NewFromConfig(AWSConfig) fsxClient := fsx.NewFromConfig(AWSConfig) glueClient := glue.NewFromConfig(AWSConfig) grafanaClient := grafana.NewFromConfig(AWSConfig) iamClient := iam.NewFromConfig(AWSConfig) + kinesisClient := kinesis.NewFromConfig(AWSConfig) lambdaClient := lambda.NewFromConfig(AWSConfig) lightsailClient := lightsail.NewFromConfig(AWSConfig) mqClient := mq.NewFromConfig(AWSConfig) @@ -1296,41 +1382,55 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("Getting a lay of the land, aka \"What regions is this account using?\"")) inventory2 := aws.Inventory2Module{ - APIGatewayClient: apiGatewayClient, - APIGatewayv2Client: apiGatewayv2Client, - AppRunnerClient: appRunnerClient, - CloudFormationClient: cloudFormationClient, - CloudfrontClient: cloudfrontClient, - CodeBuildClient: codeBuildClient, - DynamoDBClient: dynamodbClient, - EC2Client: ec2Client, - ECSClient: ecsClient, - EKSClient: eksClient, - ELBClient: elbClient, - ELBv2Client: elbv2Client, - GlueClient: glueClient, - GrafanaClient: grafanaClient, - IAMClient: iamClient, - LambdaClient: lambdaClient, - LightsailClient: lightsailClient, - MQClient: mqClient, - OpenSearchClient: openSearchClient, - RDSClient: rdsClient, - RedshiftClient: redshiftClient, - S3Client: s3Client, - SecretsManagerClient: secretsManagerClient, - SNSClient: snsClient, - SQSClient: sqsClient, - SSMClient: ssmClient, - StepFunctionClient: stepFunctionClient, - - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - } - inventory2.PrintInventoryPerRegion(AWSOutputFormat, AWSOutputDirectory, Verbosity) + APIGatewayClient: apiGatewayClient, + APIGatewayv2Client: apiGatewayv2Client, + AppRunnerClient: appRunnerClient, + AthenaClient: athenaClient, + Cloud9Client: cloud9Client, + CloudFormationClient: cloudFormationClient, + CloudfrontClient: cloudfrontClient, + CodeArtifactClient: codeArtifactClient, + CodeBuildClient: codeBuildClient, + CodeCommitClient: codeCommitClient, + CodeDeployClient: codeDeployClient, + DataPipelineClient: dataPipelineClient, + DynamoDBClient: dynamodbClient, + EC2Client: ec2Client, + ECSClient: ecsClient, + ECRClient: ecrClient, + EKSClient: eksClient, + ELBClient: elbClient, + ELBv2Client: elbv2Client, + ElasticacheClient: elasticacheClient, + ElasticBeanstalkClient: elasticBeanstalkClient, + EMRClient: emrClient, + GlueClient: glueClient, + GrafanaClient: grafanaClient, + IAMClient: iamClient, + KinesisClient: kinesisClient, + LambdaClient: lambdaClient, + LightsailClient: lightsailClient, + MQClient: mqClient, + OpenSearchClient: openSearchClient, + RDSClient: rdsClient, + RedshiftClient: redshiftClient, + Route53Client: route53Client, + S3Client: s3Client, + SecretsManagerClient: secretsManagerClient, + SNSClient: snsClient, + SQSClient: sqsClient, + SSMClient: ssmClient, + StepFunctionClient: stepFunctionClient, + + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + } + inventory2.PrintInventoryPerRegion(AWSOutputDirectory, Verbosity) tagsMod := aws.TagsModule{ ResourceGroupsTaggingApiInterface: resourceClient, @@ -1341,7 +1441,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { MaxResourcesPerRegion: 1000, } var verbosityOverride int = 1 - tagsMod.PrintTags(AWSOutputFormat, AWSOutputDirectory, verbosityOverride) + tagsMod.PrintTags(AWSOutputDirectory, verbosityOverride) orgMod := aws.OrgModule{ OrganizationsClient: orgClient, @@ -1350,8 +1450,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { AWSProfile: profile, Goroutines: Goroutines, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - orgMod.PrintOrgAccounts(AWSOutputFormat, AWSOutputDirectory, Verbosity) + orgMod.PrintOrgAccounts(AWSOutputDirectory, Verbosity) // 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.")) @@ -1364,8 +1466,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { UserDataAttributesOnly: false, AWSProfile: profile, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - instances.Instances(InstancesFilter, AWSOutputFormat, AWSOutputDirectory, Verbosity) + instances.Instances(InstancesFilter, AWSOutputDirectory, Verbosity) route53 := aws.Route53Module{ Route53Client: route53Client, @@ -1384,21 +1488,25 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - lambdasMod.PrintLambdas(AWSOutputFormat, AWSOutputDirectory, Verbosity) + lambdasMod.PrintLambdas(AWSOutputDirectory, Verbosity) - route53.PrintRoute53(AWSOutputFormat, AWSOutputDirectory, Verbosity) + route53.PrintRoute53(AWSOutputDirectory, Verbosity) filesystems := aws.FilesystemsModule{ - EFSClient: efsClient, - FSxClient: fsxClient, - Caller: *caller, - AWSProfile: profile, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - } - filesystems.PrintFilesystems(AWSOutputFormat, AWSOutputDirectory, Verbosity) + EFSClient: efsClient, + FSxClient: fsxClient, + Caller: *caller, + AWSProfile: profile, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + } + filesystems.PrintFilesystems(AWSOutputDirectory, Verbosity) endpoints := aws.EndpointsModule{ @@ -1418,14 +1526,16 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { AppRunnerClient: appRunnerClient, LightsailClient: lightsailClient, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - endpoints.PrintEndpoints(AWSOutputFormat, AWSOutputDirectory, Verbosity) + endpoints.PrintEndpoints(AWSOutputDirectory, Verbosity) gateways := aws.ApiGwModule{ APIGatewayv2Client: apiGatewayv2Client, @@ -1438,7 +1548,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { WrapTable: AWSWrapTable, } - gateways.PrintApiGws(AWSOutputFormat, AWSOutputDirectory, Verbosity) + gateways.PrintApiGws(AWSOutputDirectory, Verbosity) databases := aws.DatabasesModule{ RDSClient: rdsClient, @@ -1450,17 +1560,16 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), Goroutines: Goroutines, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - databases.PrintDatabases(AWSOutputFormat, AWSOutputDirectory, Verbosity) + databases.PrintDatabases(AWSOutputDirectory, Verbosity) ecstasks := aws.ECSTasksModule{ - DescribeTaskDefinitionClient: ecsClient, - DescribeTasksClient: ecsClient, - ListTasksClient: ecsClient, - ListClustersClient: ecsClient, - DescribeNetworkInterfacesClient: ec2Client, - IAMClient: iamClient, + EC2Client: ec2Client, + ECSClient: ecsClient, + IAMClient: iamClient, Caller: *caller, AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), @@ -1468,8 +1577,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - ecstasks.ECSTasks(AWSOutputFormat, AWSOutputDirectory, Verbosity) + ecstasks.ECSTasks(AWSOutputDirectory, Verbosity) eksCommand := aws.EKSModule{ EKSClient: eksClient, @@ -1481,17 +1592,21 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - eksCommand.EKS(AWSOutputFormat, AWSOutputDirectory, Verbosity) + eksCommand.EKS(AWSOutputDirectory, Verbosity) elasticnetworkinterfaces := aws.ElasticNetworkInterfacesModule{ - DescribeNetworkInterfacesClient: ec2Client, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - WrapTable: AWSWrapTable, + EC2Client: ec2Client, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - elasticnetworkinterfaces.ElasticNetworkInterfaces(AWSOutputFormat, AWSOutputDirectory, Verbosity) + elasticnetworkinterfaces.ElasticNetworkInterfaces(AWSOutputDirectory, Verbosity) // Secrets section fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("Looking for secrets hidden between the seat cushions.")) @@ -1506,8 +1621,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { AWSProfile: profile, Goroutines: Goroutines, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - ec2UserData.Instances(InstancesFilter, AWSOutputFormat, AWSOutputDirectory, Verbosity) + ec2UserData.Instances(InstancesFilter, AWSOutputDirectory, Verbosity) envsMod := aws.EnvsModule{ Caller: *caller, @@ -1520,8 +1637,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { SagemakerClient: sagemakerClient, Goroutines: Goroutines, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - envsMod.PrintEnvs(AWSOutputFormat, AWSOutputDirectory, Verbosity) + envsMod.PrintEnvs(AWSOutputDirectory, Verbosity) cfMod := aws.CloudformationModule{ CloudFormationClient: cloudFormationClient, @@ -1530,8 +1649,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { AWSProfile: profile, Goroutines: Goroutines, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - cfMod.PrintCloudformationStacks(AWSOutputFormat, AWSOutputDirectory, Verbosity) + cfMod.PrintCloudformationStacks(AWSOutputDirectory, Verbosity) // CPT Enum //fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("Gathering some other info that is often useful.")) @@ -1545,46 +1666,54 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { // } buckets := aws.BucketsModule{ - S3Client: s3.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + S3Client: s3.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version)), + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - buckets.PrintBuckets(AWSOutputFormat, AWSOutputDirectory, Verbosity) + buckets.PrintBuckets(AWSOutputDirectory, Verbosity) ecr := aws.ECRModule{ - ECRClient: ecrClient, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + ECRClient: ecrClient, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - ecr.PrintECR(AWSOutputFormat, AWSOutputDirectory, Verbosity) + ecr.PrintECR(AWSOutputDirectory, Verbosity) secrets := aws.SecretsModule{ SecretsManagerClient: secretsManagerClient, SSMClient: ssmClient, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - secrets.PrintSecrets(AWSOutputFormat, AWSOutputDirectory, Verbosity) + secrets.PrintSecrets(AWSOutputDirectory, Verbosity) ram := aws.RAMModule{ - RAMClient: ramClient, - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - WrapTable: AWSWrapTable, + RAMClient: ramClient, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - ram.PrintRAM(AWSOutputFormat, AWSOutputDirectory, Verbosity) + ram.PrintRAM(AWSOutputDirectory, Verbosity) networkPorts := aws.NetworkPortsModule{ EC2Client: ec2Client, @@ -1598,24 +1727,28 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { AWSProfile: profile, Goroutines: Goroutines, AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - networkPorts.PrintNetworkPorts(AWSOutputFormat, AWSOutputDirectory) + networkPorts.PrintNetworkPorts(AWSOutputDirectory) sqsMod := aws.SQSModule{ SQSClient: sqsClient, StorePolicies: StoreSQSAccessPolicies, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - sqsMod.PrintSQS(AWSOutputFormat, AWSOutputDirectory, Verbosity) + sqsMod.PrintSQS(AWSOutputDirectory, Verbosity) cloudFoxSNSClient := aws.InitCloudFoxSNSClient(*caller, profile, cmd.Root().Version, Goroutines, AWSWrapTable) - cloudFoxSNSClient.PrintSNS(AWSOutputFormat, AWSOutputDirectory, Verbosity) + cloudFoxSNSClient.PrintSNS(AWSOutputDirectory, Verbosity) resourceTrustsCommand := aws.ResourceTrustsModule{ Caller: *caller, @@ -1624,8 +1757,10 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), WrapTable: AWSWrapTable, CloudFoxVersion: cmd.Root().Version, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - resourceTrustsCommand.PrintResources(AWSOutputFormat, AWSOutputDirectory, Verbosity) + resourceTrustsCommand.PrintResources(AWSOutputDirectory, Verbosity) codeBuildCommand := aws.CodeBuildModule{ CodeBuildClient: codeBuildClient, @@ -1634,20 +1769,24 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - codeBuildCommand.PrintCodeBuildProjects(AWSOutputFormat, AWSOutputDirectory, Verbosity) + codeBuildCommand.PrintCodeBuildProjects(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{ - IAMClient: iamClient, - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + IAMClient: iamClient, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - principals.PrintIamPrincipals(AWSOutputFormat, AWSOutputDirectory, Verbosity) + principals.PrintIamPrincipals(AWSOutputDirectory, Verbosity) permissions := aws.IamPermissionsModule{ IAMClient: iamClient, Caller: *caller, @@ -1655,15 +1794,17 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, WrapTable: AWSWrapTable, } - permissions.PrintIamPermissions(AWSOutputFormat, AWSOutputDirectory, Verbosity, PermissionsPrincipal) + permissions.PrintIamPermissions(AWSOutputDirectory, Verbosity, PermissionsPrincipal) accessKeys := aws.AccessKeysModule{ - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - } - accessKeys.PrintAccessKeys(AccessKeysFilter, AWSOutputFormat, AWSOutputDirectory, Verbosity) + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + } + accessKeys.PrintAccessKeys(AccessKeysFilter, AWSOutputDirectory, Verbosity) roleTrusts := aws.RoleTrustsModule{ IAMClient: iamClient, Caller: *caller, @@ -1671,25 +1812,31 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { Goroutines: Goroutines, SkipAdminCheck: AWSSkipAdminCheck, WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - roleTrusts.PrintRoleTrusts(AWSOutputFormat, AWSOutputDirectory, Verbosity) + roleTrusts.PrintRoleTrusts(AWSOutputDirectory, Verbosity) pmapperCommand := aws.PmapperModule{ - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - pmapperCommand.PrintPmapperData(AWSOutputFormat, AWSOutputDirectory, Verbosity) + pmapperCommand.PrintPmapperData(AWSOutputDirectory, Verbosity) iamSimulator := aws.IamSimulatorModule{ - IAMClient: iamClient, - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, + IAMClient: iamClient, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } - iamSimulator.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputFormat, AWSOutputDirectory, Verbosity) + iamSimulator.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputDirectory, Verbosity) fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("That's it! Check your output files for situational awareness and check your loot files for next steps.")) fmt.Printf("[%s] %s\n\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("FYI, we skipped the outbound-assumed-roles module in all-checks (really long run time). Make sure to try it out manually.")) @@ -1698,43 +1845,18 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { func init() { cobra.OnInitialize(initAWSProfiles) - sdk.RegisterApiGatewayTypes() - sdk.RegisterApiGatewayV2Types() - sdk.RegisterApprunnerTypes() - sdk.RegisterCloudFormationTypes() - sdk.RegisterCodeBuildTypes() - sdk.RegisterDocDBTypes() - sdk.RegisterDynamoDBTypes() - sdk.RegisterEC2Types() - sdk.RegisterECRTypes() - sdk.RegisterEFSTypes() - sdk.RegisterEKSTypes() - sdk.RegisterELBTypes() - sdk.RegisterELBv2Types() - sdk.RegisterGrafanaTypes() - sdk.RegisterIamTypes() - sdk.RegisterLambdaTypes() - sdk.RegisterLightsailTypes() - sdk.RegisterMQTypes() - sdk.RegisterOpenSearchTypes() - sdk.RegisterOrganizationsTypes() - sdk.RegisterRDSTypes() - sdk.RegisterRedShiftTypes() - sdk.RegisterS3Types() - sdk.RegisterSecretsManagerTypes() - sdk.RegisterStepFunctionsTypes() // Role Trusts Module Flags - RoleTrustCommand.Flags().StringVarP(&RoleTrustFilter, "filter", "t", "all", "[AccountNumber | PrincipalARN | PrincipalName | ServiceName]") + RoleTrustCommand.Flags().StringVarP(&RoleTrustFilter, "filter", "f", "all", "[AccountNumber | PrincipalARN | PrincipalName | ServiceName]") // Map Access Keys Module Flags - AccessKeysCommand.Flags().StringVarP(&AccessKeysFilter, "filter", "t", "none", "Access key ID to search for") + AccessKeysCommand.Flags().StringVarP(&AccessKeysFilter, "filter", "f", "none", "Access key ID to search for") // IAM Simulator Module Flags //IamSimulatorCommand.Flags().StringVarP(&IamSimulatorFilter, "filter", "f", "none", "Access key ID to search for") // Instances Map Module Flags - InstancesCommand.Flags().StringVarP(&InstancesFilter, "filter", "t", "all", "[InstanceID | InstanceIDsFile]") + InstancesCommand.Flags().StringVarP(&InstancesFilter, "filter", "f", "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 @@ -1757,18 +1879,22 @@ func init() { // 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.") + // buckets command flags (for bucket policies) + BucketsCommand.Flags().BoolVarP(&CheckBucketPolicies, "with-policies", "", false, "Analyze bucket policies (this is already done in the resource-trusts command)") + // 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") AWSCommands.PersistentFlags().BoolVarP(&AWSAllProfiles, "all-profiles", "a", false, "Use all AWS CLI profiles in AWS credentials file") AWSCommands.PersistentFlags().BoolVarP(&AWSConfirm, "yes", "y", false, "Non-interactive mode (like apt/yum)") - AWSCommands.PersistentFlags().StringVarP(&AWSOutputFormat, "output", "o", "brief", "[\"brief\" | \"wide\" ]") + AWSCommands.PersistentFlags().StringVarP(&AWSOutputType, "output", "o", "brief", "[\"brief\" | \"wide\" ]") AWSCommands.PersistentFlags().IntVarP(&Verbosity, "verbosity", "v", 1, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n") AWSCommands.PersistentFlags().StringVar(&AWSOutputDirectory, "outdir", defaultOutputDir, "Output Directory ") AWSCommands.PersistentFlags().IntVarP(&Goroutines, "max-goroutines", "g", 30, "Maximum number of concurrent goroutines") AWSCommands.PersistentFlags().BoolVar(&AWSSkipAdminCheck, "skip-admin-check", false, "Skip check to determine if role is an Admin") AWSCommands.PersistentFlags().BoolVarP(&AWSWrapTable, "wrap", "w", false, "Wrap table to fit in terminal (complicates grepping)") AWSCommands.PersistentFlags().BoolVarP(&AWSUseCache, "cached", "c", false, "Load cached data from disk. Faster, but if changes have been recently made you'll miss them") + AWSCommands.PersistentFlags().StringVarP(&AWSTableCols, "cols", "t", "", "Comma separated list of columns to display in table output") AWSCommands.AddCommand( AllChecksCommand, diff --git a/cli/azure.go b/cli/azure.go index d40198b..b98bd55 100644 --- a/cli/azure.go +++ b/cli/azure.go @@ -9,12 +9,13 @@ import ( var ( AzTenantID string - AzSubscriptionID string + AzSubscription string AzRGName string AzOutputFormat string AzOutputDirectory string AzVerbosity int AzWrapTable bool + AzMergedTable bool AzCommands = &cobra.Command{ Use: "azure", @@ -25,7 +26,8 @@ var ( cmd.Help() }, } - AzWhoamiCommand = &cobra.Command{ + AzWhoamiListRGsAlso bool + AzWhoamiCommand = &cobra.Command{ Use: "whoami", Aliases: []string{}, Short: "Display available Azure CLI sessions", @@ -33,7 +35,7 @@ var ( Display Available Azure CLI Sessions: ./cloudfox az whoami`, Run: func(cmd *cobra.Command, args []string) { - err := azure.AzWhoamiCommand(cmd.Root().Version, AzWrapTable) + err := azure.AzWhoamiCommand(AzOutputDirectory, cmd.Root().Version, AzWrapTable, AzVerbosity, AzWhoamiListRGsAlso) if err != nil { log.Fatal(err) } @@ -51,7 +53,7 @@ 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) + err := azure.AzInventoryCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable) if err != nil { log.Fatal(err) } @@ -69,24 +71,25 @@ Enumerate role assignments for a specific subscription: ./cloudfox az rbac --subscription SUBSCRIPTION_ID `, Run: func(cmd *cobra.Command, args []string) { - err := azure.AzRBACCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable) + + err := azure.AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable) if err != nil { log.Fatal(err) } }, } - AzInstancesCommand = &cobra.Command{ - Use: "instances", - Aliases: []string{}, - Short: "Enumerates Azure Compute instances", + AzVMsCommand = &cobra.Command{ + Use: "vms", + Aliases: []string{"vms", "virtualmachines"}, + Short: "Enumerates Azure Compute virtual machines", Long: ` Enumerate VMs for a specific tenant: -./cloudfox az instances --tenant TENANT_ID +./cloudfox az vms --tenant TENANT_ID Enumerate VMs for a specific subscription: -./cloudfox az instances --subscription SUBSCRIPTION_ID`, +./cloudfox az vms --subscription SUBSCRIPTION_ID`, Run: func(cmd *cobra.Command, args []string) { - err := azure.AzInstancesCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable) + err := azure.AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable) if err != nil { log.Fatal(err) } @@ -104,7 +107,7 @@ Enumerate storage accounts for a specific subscription: ./cloudfox az storage --subscription SUBSCRIPTION_ID `, Run: func(cmd *cobra.Command, args []string) { - err := azure.AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable) + err := azure.AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable) if err != nil { log.Fatal(err) } @@ -113,48 +116,24 @@ Enumerate storage accounts for a specific subscription: ) func init() { + + AzWhoamiCommand.Flags().BoolVarP(&AzWhoamiListRGsAlso, "list-rgs", "l", false, "Drill down to the resource group level") + // Global flags - AzCommands.PersistentFlags().StringVarP( - &AzOutputFormat, - "output", - "o", - "all", - "[\"table\" | \"csv\" | \"all\" ]") - AzCommands.PersistentFlags().IntVarP( - &AzVerbosity, - "verbosity", - "v", - 2, - "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n") - AzCommands.PersistentFlags().StringVarP( - &AzTenantID, - "tenant", - "t", - "", - "Tenant name") - AzCommands.PersistentFlags().StringVarP( - &AzSubscriptionID, - "subscription", - "s", - "", - "Subscription name") - AzCommands.PersistentFlags().StringVarP( - &AzRGName, - "resource-group", - "g", - "", - "Resource Group name") - AzCommands.PersistentFlags().BoolVarP( - &AzWrapTable, - "wrap", - "w", - false, - "Wrap table to fit in terminal (complicates grepping)") + AzCommands.PersistentFlags().StringVarP(&AzOutputFormat, "output", "o", "all", "[\"table\" | \"csv\" | \"all\" ]") + AzCommands.PersistentFlags().StringVar(&AzOutputDirectory, "outdir", defaultOutputDir, "Output Directory ") + AzCommands.PersistentFlags().IntVarP(&AzVerbosity, "verbosity", "v", 2, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n") + AzCommands.PersistentFlags().StringVarP(&AzTenantID, "tenant", "t", "", "Tenant name") + AzCommands.PersistentFlags().StringVarP(&AzSubscription, "subscription", "s", "", "Subscription ID or Name") + AzCommands.PersistentFlags().StringVarP(&AzRGName, "resource-group", "g", "", "Resource Group name") + AzCommands.PersistentFlags().BoolVarP(&AzWrapTable, "wrap", "w", false, "Wrap table to fit in terminal (complicates grepping)") + AzCommands.PersistentFlags().BoolVarP(&AzMergedTable, "merged-table", "m", false, "Writes a single table for all subscriptions in the tenant. Default writes a table per subscription.") AzCommands.AddCommand( AzWhoamiCommand, AzRBACCommand, - AzInstancesCommand, + AzVMsCommand, AzStorageCommand, AzInventoryCommand) + } diff --git a/globals/azure.go b/globals/azure.go index 8082d93..7dfa570 100644 --- a/globals/azure.go +++ b/globals/azure.go @@ -18,7 +18,7 @@ var AAD_USERS_TEST_FILE string // Module names const AZ_WHOAMI_MODULE_NAME = "whoami" const AZ_INVENTORY_MODULE_NAME = "inventory" -const AZ_INSTANCES_MODULE_NAME = "instances" +const AZ_VMS_MODULE_NAME = "vms" const AZ_RBAC_MODULE_NAME = "rbac" const AZ_STORAGE_MODULE_NAME = "storage" diff --git a/globals/utils.go b/globals/utils.go index f7b48f6..ee913aa 100644 --- a/globals/utils.go +++ b/globals/utils.go @@ -2,5 +2,5 @@ package globals const CLOUDFOX_USER_AGENT = "cloudfox" const CLOUDFOX_LOG_FILE_DIR_NAME = ".cloudfox" -const CLOUDFOX_BASE_DIRECTORY = ".cloudfox/cloudfox-output" +const CLOUDFOX_BASE_DIRECTORY = "cloudfox-output" const LOOT_DIRECTORY_NAME = "loot" diff --git a/go.mod b/go.mod index 1a5e4e7..d4c8d1e 100644 --- a/go.mod +++ b/go.mod @@ -4,67 +4,70 @@ go 1.20 require ( github.com/Azure/azure-sdk-for-go v68.0.0+incompatible - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 github.com/Azure/go-autorest/autorest v0.11.29 github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 github.com/aquasecurity/table v1.8.0 - github.com/aws/aws-sdk-go-v2 v1.18.0 - github.com/aws/aws-sdk-go-v2/config v1.18.25 - github.com/aws/aws-sdk-go-v2/service/apigateway v1.16.11 - github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.13.11 - github.com/aws/aws-sdk-go-v2/service/apprunner v1.17.9 - github.com/aws/aws-sdk-go-v2/service/cloudformation v1.29.0 - github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.6 - github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.26.0 - github.com/aws/aws-sdk-go-v2/service/codebuild v1.20.13 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.7 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 - github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11 - github.com/aws/aws-sdk-go-v2/service/ecs v1.27.1 - github.com/aws/aws-sdk-go-v2/service/efs v1.20.1 - github.com/aws/aws-sdk-go-v2/service/eks v1.27.12 - github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.0 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.15.10 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.19.11 - github.com/aws/aws-sdk-go-v2/service/fsx v1.28.13 - github.com/aws/aws-sdk-go-v2/service/glue v1.50.0 - github.com/aws/aws-sdk-go-v2/service/grafana v1.13.1 - github.com/aws/aws-sdk-go-v2/service/iam v1.20.0 - github.com/aws/aws-sdk-go-v2/service/lambda v1.35.0 - github.com/aws/aws-sdk-go-v2/service/lightsail v1.26.6 - github.com/aws/aws-sdk-go-v2/service/mq v1.14.11 - github.com/aws/aws-sdk-go-v2/service/opensearch v1.17.0 - github.com/aws/aws-sdk-go-v2/service/organizations v1.19.6 - github.com/aws/aws-sdk-go-v2/service/ram v1.18.2 - github.com/aws/aws-sdk-go-v2/service/rds v1.45.0 - github.com/aws/aws-sdk-go-v2/service/redshift v1.27.11 - github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.14.12 - github.com/aws/aws-sdk-go-v2/service/route53 v1.28.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 - github.com/aws/aws-sdk-go-v2/service/sagemaker v1.83.0 - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.8 - github.com/aws/aws-sdk-go-v2/service/sns v1.20.11 - github.com/aws/aws-sdk-go-v2/service/sqs v1.23.0 - github.com/aws/aws-sdk-go-v2/service/ssm v1.36.4 - github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 - github.com/aws/smithy-go v1.13.5 + github.com/aws/aws-sdk-go-v2 v1.21.2 + github.com/aws/aws-sdk-go-v2/config v1.19.1 + github.com/aws/aws-sdk-go-v2/service/apigateway v1.18.2 + github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.15.0 + github.com/aws/aws-sdk-go-v2/service/apprunner v1.21.2 + github.com/aws/aws-sdk-go-v2/service/cloud9 v1.18.11 + github.com/aws/aws-sdk-go-v2/service/cloudformation v1.36.0 + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.28.7 + github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.29.2 + github.com/aws/aws-sdk-go-v2/service/codebuild v1.22.2 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.23.0 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.127.0 + github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 + github.com/aws/aws-sdk-go-v2/service/ecs v1.30.4 + github.com/aws/aws-sdk-go-v2/service/efs v1.21.9 + github.com/aws/aws-sdk-go-v2/service/eks v1.30.0 + github.com/aws/aws-sdk-go-v2/service/elasticache v1.30.0 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.18.0 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.22.0 + github.com/aws/aws-sdk-go-v2/service/fsx v1.34.0 + github.com/aws/aws-sdk-go-v2/service/glue v1.64.0 + github.com/aws/aws-sdk-go-v2/service/grafana v1.15.7 + github.com/aws/aws-sdk-go-v2/service/iam v1.23.0 + github.com/aws/aws-sdk-go-v2/service/lambda v1.41.0 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.29.0 + github.com/aws/aws-sdk-go-v2/service/mq v1.17.0 + github.com/aws/aws-sdk-go-v2/service/opensearch v1.22.0 + github.com/aws/aws-sdk-go-v2/service/organizations v1.20.8 + github.com/aws/aws-sdk-go-v2/service/ram v1.20.7 + github.com/aws/aws-sdk-go-v2/service/rds v1.58.0 + github.com/aws/aws-sdk-go-v2/service/redshift v1.31.0 + github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.16.2 + github.com/aws/aws-sdk-go-v2/service/route53 v1.31.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2 + github.com/aws/aws-sdk-go-v2/service/sagemaker v1.112.0 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.6 + github.com/aws/aws-sdk-go-v2/service/sns v1.22.2 + github.com/aws/aws-sdk-go-v2/service/sqs v1.24.7 + github.com/aws/aws-sdk-go-v2/service/ssm v1.40.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 + github.com/aws/smithy-go v1.15.0 github.com/bishopfox/awsservicemap v1.0.2 - github.com/dominikbraun/graph v0.22.2 + github.com/dominikbraun/graph v0.23.0 github.com/fatih/color v1.15.0 github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/kyokomi/emoji v2.2.4+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible github.com/sirupsen/logrus v1.9.3 - github.com/spf13/afero v1.9.5 + github.com/spf13/afero v1.10.0 github.com/spf13/cobra v1.7.0 - golang.org/x/crypto v0.9.0 + golang.org/x/crypto v0.14.0 ) +require github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.4.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect @@ -73,44 +76,52 @@ 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 v1.0.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.24 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect - github.com/aws/aws-sdk-go-v2/service/docdb v1.21.3 - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sfn v1.17.11 - github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6 // indirect + github.com/aws/aws-sdk-go-v2/service/athena v1.32.0 + github.com/aws/aws-sdk-go-v2/service/codeartifact v1.20.2 + github.com/aws/aws-sdk-go-v2/service/codecommit v1.16.4 + github.com/aws/aws-sdk-go-v2/service/codedeploy v1.18.3 + github.com/aws/aws-sdk-go-v2/service/datapipeline v1.16.2 + github.com/aws/aws-sdk-go-v2/service/docdb v1.24.0 + github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk v1.17.2 + github.com/aws/aws-sdk-go-v2/service/emr v1.29.0 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.37 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6 // indirect + github.com/aws/aws-sdk-go-v2/service/kinesis v1.20.0 + github.com/aws/aws-sdk-go-v2/service/sfn v1.19.8 + github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/go-openapi/errors v0.20.3 // indirect + github.com/go-openapi/errors v0.20.4 // indirect github.com/go-openapi/strfmt v0.21.7 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // 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.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // 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-20210911075715-681adbf594b8 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect - go.mongodb.org/mongo-driver v1.11.7 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/term v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + go.mongodb.org/mongo-driver v1.12.1 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index 09b3d0d..5708efa 100644 --- a/go.sum +++ b/go.sum @@ -38,18 +38,19 @@ 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 v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1 h1:SEy2xmstIphdPwNBUi7uhvjyjhVKISfwjfOJmuy7kg4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 h1:9kDVnTz3vbfweTqAUmk/a/pH5pWFCHtvRpHYC0G/dcA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.4.0 h1:TuEMD+E+1aTjjLICGQOW6vLe8UWES7kopac9mUXL56Y= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.4.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 h1:gggzg0SUMs6SQbEw+3LoSsYf9YMjkupeAnHMX8O9mmY= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgxoBDMqMO/Nvy7bZ9a0nbU3I1DtFQK3YvB4= 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= @@ -77,126 +78,144 @@ 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 v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 h1:hVeq+yCyUi+MsoO/CU95yqCIcdzra5ovzk8Q2BBpV2M= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 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= github.com/aquasecurity/table v1.8.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY= -github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= -github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q= -github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4= -github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0= -github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g= -github.com/aws/aws-sdk-go-v2/service/apigateway v1.16.11 h1:90Jf9htQw8RA1FIujUc8ZM8AFWNySHZH1SKe2/0Kla8= -github.com/aws/aws-sdk-go-v2/service/apigateway v1.16.11/go.mod h1:GXjIkQpFivo8T4szSTIiNQBvONXQz/MbN+M251q9BPk= -github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.13.11 h1:1L2042GftNVyI3TtWclGodfN5zBQjBNXsTQxDNaPXs8= -github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.13.11/go.mod h1:Cs+mG0DXkVYPWsWIE8Ga78C/HeN5zFBbPHdOnJPwZ4M= -github.com/aws/aws-sdk-go-v2/service/apprunner v1.17.9 h1:Ddc9R2j1KQ4qhbbfwugcQ44LzmeieaUSlBGbAGASuLM= -github.com/aws/aws-sdk-go-v2/service/apprunner v1.17.9/go.mod h1:tKdk26xmO22Q+geNJM7/uMDdTMvPCFZM12H0m1jpsHM= -github.com/aws/aws-sdk-go-v2/service/cloudformation v1.29.0 h1:MjDK6nt3iDPCk4CVLrc6GoxZIunzRnyIalTYwEUKb/E= -github.com/aws/aws-sdk-go-v2/service/cloudformation v1.29.0/go.mod h1:YtA9SsNBWnaDpSECATt8ghAOUMcGeHcnY2kTENLNmO8= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.6 h1:qDEv2jw4sFWOxpo++zFx76GQ7MEh+Q7gHg7/6PVFMJs= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.6/go.mod h1:VaCrMUb08RMUN5p3ss+92ZEGC/2NkWuiCNYET3xkirw= -github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.26.0 h1:tPKUl76gWGmmdewjbAMlbL32hr6roDoE8xLiqKNGPfo= -github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.26.0/go.mod h1:JJDbUhySZXBEEMEMtiNubwi7ooh7OXI8mb/ItEAShjs= -github.com/aws/aws-sdk-go-v2/service/codebuild v1.20.13 h1:EPmZulYYBXFr/TlIe4iPMm6nyR198VMJOur0EzHvTvI= -github.com/aws/aws-sdk-go-v2/service/codebuild v1.20.13/go.mod h1:5Wsxbl13SRHZ3oUQqIjvn+wLwiknRA/Myi1F5fGW6mo= -github.com/aws/aws-sdk-go-v2/service/docdb v1.21.3 h1:3KemQEVSRTEMexSmRVgqYSLqClvhtETjmrxxQhNLmuo= -github.com/aws/aws-sdk-go-v2/service/docdb v1.21.3/go.mod h1:XC0nkyZ9G1cFKU/K0WS/Jhrz6fsfy411T2L0MEFIlMo= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.7 h1:yb2o8oh3Y+Gg2g+wlzrWS3pB89+dHrXayT/d9cs8McU= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.7/go.mod h1:1MNss6sqoIsFGisX92do/5doiUCBrN7EjhZCS/8DUjI= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 h1:NXi4pNJWjAaiI56P1Rl8DC9A4jMNRE00WNBsDua5WRg= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0/go.mod h1:L3ZT0N/vBsw77mOAawXmRnREpEjcHd2v5Hzf7AkIH8M= -github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11 h1:wlTgmb/sCmVRJrN5De3CiHj4v/bTCgL5+qpdEd0CPtw= -github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11/go.mod h1:Ce1q2jlNm8BVpjLaOnwnm5v2RClAbK6txwPljFzyW6c= -github.com/aws/aws-sdk-go-v2/service/ecs v1.27.1 h1:54QSuWR3Pot7HqBRXd+c1yF97h2bqzDBID8qFSAkTlE= -github.com/aws/aws-sdk-go-v2/service/ecs v1.27.1/go.mod h1:SB6YszwN1iKvyt/Qk+ICeKsfBxjd0CTEwwkmej9qoa0= -github.com/aws/aws-sdk-go-v2/service/efs v1.20.1 h1:fJFdGkf0Xj04IBnps3YlL6mGPSjzfgAZ7ZX+mbsrPDc= -github.com/aws/aws-sdk-go-v2/service/efs v1.20.1/go.mod h1:7szMjYu35IWLaJEG23z6x/KOFckAF4/oQdNuDni99Pk= -github.com/aws/aws-sdk-go-v2/service/eks v1.27.12 h1:eKidf2ebtleLtH67x02syhO9t3FItv9a7/ep9KC3TAM= -github.com/aws/aws-sdk-go-v2/service/eks v1.27.12/go.mod h1:ZoyBDE311XYRiJpofw4jorVH2u+UhFpzfkrxF3aWu0U= -github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.0 h1:m0ZU6thCelJgtyNMnXYp39y/SY9qE4clxhs84sQM3jA= -github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.0/go.mod h1:uSAGbZNsmIG2EXhWd7+6iKtwF15wWNtR5HLuRamlY5Q= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.15.10 h1:gGqHXu9rt/F+xGidPfFKVZUYEDZ3zKMMAOx1yVUr//U= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.15.10/go.mod h1:pidEyxe4u/vkB8wvbKRZ/r6IUJcyhQoTbSLA2HWR6cY= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.19.11 h1:IN2XMTLmhIEL5e3o+tY9JsLFSAxmjgM8gI7W2+CPrpw= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.19.11/go.mod h1:oPHYtcocUcfHOE7qygtvyZMw82nedCKZSop/R9jxlAM= -github.com/aws/aws-sdk-go-v2/service/fsx v1.28.13 h1:lIVHlpyYIQPjFX47bsf6u0vfMLP1A4a7imoqzmIj66s= -github.com/aws/aws-sdk-go-v2/service/fsx v1.28.13/go.mod h1:jQE07g3nDTABqgZabycvM3LsgnUYRImP4zWPt4Sb9Bc= -github.com/aws/aws-sdk-go-v2/service/glue v1.50.0 h1:GF6Lsy9g1+Ig2e1TpGygl00+oBcdYHIMyTHoKZa9VGE= -github.com/aws/aws-sdk-go-v2/service/glue v1.50.0/go.mod h1:agadckFdb7BwFqeN4CXt3yrMtoFvY/8b2F+8FNeHVOc= -github.com/aws/aws-sdk-go-v2/service/grafana v1.13.1 h1:7njo8N0P8cfchwXTn1xefRbYCzjQN6jrqOATFoan37U= -github.com/aws/aws-sdk-go-v2/service/grafana v1.13.1/go.mod h1:FkpbGiG/BBvaZKQTy4eNxo04PJXOuGwIbVMuUM+xAUo= -github.com/aws/aws-sdk-go-v2/service/iam v1.20.0 h1:ywXSXkssdnuPlJyCZVO5kAUQhFm/RhsbvWRHklJ0uH4= -github.com/aws/aws-sdk-go-v2/service/iam v1.20.0/go.mod h1:kAnokExGCYs7zfvZEZdFHvQ/x4ZKIci0Raps6mZI1Ag= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27 h1:QmyPCRZNMR1pFbiOi9kBZWZuKrKB9LD4cxltxQk4tNE= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27/go.mod h1:DfuVY36ixXnsG+uTqnoLWunXAKJ4qjccoFrXUPpj+hs= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM= -github.com/aws/aws-sdk-go-v2/service/lambda v1.35.0 h1:iNLsDIOju/bbqw0mNaEXh+9Ms6Mm0RjcHPP9z4k9lUY= -github.com/aws/aws-sdk-go-v2/service/lambda v1.35.0/go.mod h1:i23nHcGEyswthctBfhEO1agGpM5Uyh83aSmSB6DmdCk= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.26.6 h1:QQE/ZcXSrPFGprrG8VFblHiMpenvzICT09YnaMmQEwk= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.26.6/go.mod h1:L+JqH2pSCvKnCVJNKnU/8TTUfuNuTXSmXiS3F0zMvzQ= -github.com/aws/aws-sdk-go-v2/service/mq v1.14.11 h1:eZ1ScRKjMgz9yHTFBivnnRW9eUAoGXeuvWmx28ACYbI= -github.com/aws/aws-sdk-go-v2/service/mq v1.14.11/go.mod h1:ladDWyHbOQSExyg5caiKPBAxkjZfAbb/wZcXpF80IB8= -github.com/aws/aws-sdk-go-v2/service/opensearch v1.17.0 h1:GAFqcrOqxNh/2FXEb07nBpqlv79hGxJoI1u+5Cl/aAU= -github.com/aws/aws-sdk-go-v2/service/opensearch v1.17.0/go.mod h1:a+o5iMtYkg8xyE1VLRmhXmWElEgugcSIlU3rCY9hL5g= -github.com/aws/aws-sdk-go-v2/service/organizations v1.19.6 h1:wHV9iUDPdluHAkeJBP9exp4IO9KN+T7/UHgltB8Udsg= -github.com/aws/aws-sdk-go-v2/service/organizations v1.19.6/go.mod h1:zw4Ac19gtzc4cdtfBCTZa7FlrXYh8tbUl3Jr7movexs= -github.com/aws/aws-sdk-go-v2/service/ram v1.18.2 h1:taFrZuOwM1UiayFQhSXw1JX5v2wFM/qIBGxgN5UWsxU= -github.com/aws/aws-sdk-go-v2/service/ram v1.18.2/go.mod h1:lYxryEoqZQtttq5iIoA40FD079MxYvNCIzczvMD9iAM= -github.com/aws/aws-sdk-go-v2/service/rds v1.45.0 h1:Yi23UNiGidNfT7tIW0lbE6JtRR1ZN+cNZGRTKLB+opk= -github.com/aws/aws-sdk-go-v2/service/rds v1.45.0/go.mod h1:rS6T0DrjdZ5LDr8ZC/J9iZdD1oSbie5reWWzqv5zLOw= -github.com/aws/aws-sdk-go-v2/service/redshift v1.27.11 h1:a6XfWSH3xO0+eEAT9ox6V13LNX09fQrs7QvU5+juxY0= -github.com/aws/aws-sdk-go-v2/service/redshift v1.27.11/go.mod h1:gEfU7N8/eImCoodwl5CKW7Mih19THtJhzQCdA5TZZQ0= -github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.14.12 h1:/fvzwRqxxiNJwNK5XVE1NvnQFvNajNRYx82lkTqXlMM= -github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.14.12/go.mod h1:i/CXT+v8caoM60a7brYlrVG64srBa1Op0Q5Nl6askyo= -github.com/aws/aws-sdk-go-v2/service/route53 v1.28.1 h1:8e1fgdyer5IqBPtiWNsVLY/XFucmNTtYMqADyCFXTgQ= -github.com/aws/aws-sdk-go-v2/service/route53 v1.28.1/go.mod h1:9SEpwqaALzp34eCT6w5PTh4SDDT84wxfMRx9VJSJPsk= -github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8= -github.com/aws/aws-sdk-go-v2/service/sagemaker v1.83.0 h1:8fsILyGP7AMoK4LkO09UE9x/6uj05i3N3//CqUE3z1E= -github.com/aws/aws-sdk-go-v2/service/sagemaker v1.83.0/go.mod h1:l4Wxb+ZaxOd9hpjFmzI28NgX8us5H3rv/QXg9q/T8tE= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.8 h1:eB91eEYUlh8+O2dXr189W8GJJd+/T8N/c5HocH2KzVo= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.8/go.mod h1:3ARttS6G6U3auEdKfaN4GlnfS9UxYE9nqub1+0YGycA= -github.com/aws/aws-sdk-go-v2/service/sfn v1.17.11 h1:A3Y64jN5O4kZMDpsddKgy7p5ZRmKae4Rd5JJglkIq5Q= -github.com/aws/aws-sdk-go-v2/service/sfn v1.17.11/go.mod h1:pZ4bJEoEyKsCxq1IJFbhiB3JKNr1VMvmI+ujmlwOiuU= -github.com/aws/aws-sdk-go-v2/service/sns v1.20.11 h1:kUKAkuOhCCq/Av372Dtzg0oaAD5VEUYdDtU4lGIYKkw= -github.com/aws/aws-sdk-go-v2/service/sns v1.20.11/go.mod h1:WjBcrd28zNbbuAcIRO/n89sSeOxTuOZPiuxNXU/2WrI= -github.com/aws/aws-sdk-go-v2/service/sqs v1.23.0 h1:EgyGgs20+tdc2F2P7mKCD6SkWv/62fsGZlT3N5VFi5M= -github.com/aws/aws-sdk-go-v2/service/sqs v1.23.0/go.mod h1:ujUjm+PrcKUeIiKu2PT7MWjcyY0D6YZRZF3fSswiO+0= -github.com/aws/aws-sdk-go-v2/service/ssm v1.36.4 h1:3AjvCuRS8OnNVRC/UBagp1Jo2feR94+VAIKO4lz8gOQ= -github.com/aws/aws-sdk-go-v2/service/ssm v1.36.4/go.mod h1:p6MaesK9061w6NTiFmZpUzEkKUY5blKlwD2zYyErxKA= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= -github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E= -github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= -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/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= +github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14 h1:Sc82v7tDQ/vdU1WtuSyzZ1I7y/68j//HJ6uozND1IDs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14/go.mod h1:9NCTOURS8OpxvoAVHq79LK81/zC78hfRWFn+aL0SPcY= +github.com/aws/aws-sdk-go-v2/config v1.19.1 h1:oe3vqcGftyk40icfLymhhhNysAwk0NfiwkDi2GTPMXs= +github.com/aws/aws-sdk-go-v2/config v1.19.1/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6 h1:wmGLw2i8ZTlHLw7a9ULGfQbuccw8uIiNr6sol5bFzc8= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6/go.mod h1:Q0Hq2X/NuL7z8b1Dww8rmOFl+jzusKEcyvkKspwdpyc= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.18.2 h1:d1T1J5oR9zHMHbXMs22nxR6YCBQEf0rAHyaZvv34XEM= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.18.2/go.mod h1:hfIgsvZ+nvnfBqu6T1H1YTeFkkPr0I/+UTyekLl054E= +github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.15.0 h1:q6PifcrJdhfbpARjoMRxVDzOJWBzkM0aBtewbAHYpMI= +github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.15.0/go.mod h1:MfSzpqLwryvxfGpwi9w2Re7DqkLG4qv+qzGu9a+GKJ0= +github.com/aws/aws-sdk-go-v2/service/apprunner v1.21.2 h1:n2SWTOTKAlh5Wi78ypl8dy3iW0kFe3OATPDrf2pjkhE= +github.com/aws/aws-sdk-go-v2/service/apprunner v1.21.2/go.mod h1:GvCOU2jyq+MgUD5v99Lw/jjjC0NKKCq7LVDeebOpGj4= +github.com/aws/aws-sdk-go-v2/service/athena v1.32.0 h1:n4ui9xL0VbmOVNi2+hiWIrVLVhew/1/4BdzjJY3Px58= +github.com/aws/aws-sdk-go-v2/service/athena v1.32.0/go.mod h1:vKT/ZbLHt0S5hlmf0T5OzcPhs4LJoEmqlr49JP+9Hy8= +github.com/aws/aws-sdk-go-v2/service/cloud9 v1.18.11 h1:stATlP32EI69+sNDOKw30IbEhgXAu6bxwM/NvQcA1SU= +github.com/aws/aws-sdk-go-v2/service/cloud9 v1.18.11/go.mod h1:e1xHnjdRt/ZaqrtO6gUwpbtPF8PtkRVyN1zCmz1IbNI= +github.com/aws/aws-sdk-go-v2/service/cloudformation v1.36.0 h1:9ls+8DHLhF0E4/xtZDNF3iYY9Ibqh+fG1y2ueKFDeEo= +github.com/aws/aws-sdk-go-v2/service/cloudformation v1.36.0/go.mod h1:EV06EPuSb3m40bD1suX/QSj3o161aG/6Wwbodk2vqzA= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.28.7 h1:rbzZROs9b8Fpo0jtdPy2Qfxh8SRAAXx6sEMX4yxqF6g= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.28.7/go.mod h1:0rod2DMcm5wuAlNJn99o/dtwuQKul9asVbcUn4nBIZs= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.29.2 h1:S9sfsAGkT/5e0bzqLMK7k3DkoZEz0EioL336dT+sCIM= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.29.2/go.mod h1:N6mA9PPSL2C2drFl+Yx8oEUiuSGhOiplOixRGWc2OYM= +github.com/aws/aws-sdk-go-v2/service/codeartifact v1.20.2 h1:iOcHl9tgrHxvPVOW1cRHwIj9T7/FrkPE6bM0aDKOxE8= +github.com/aws/aws-sdk-go-v2/service/codeartifact v1.20.2/go.mod h1:dZQurYLoNZG5Px5uhyf64g2th0tYkGihn+0pXOmMA0Y= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.22.2 h1:Xg0xMsQYw/8g+A6RQs6w9HtwQ5SgDojYjYbnKFmnp0Y= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.22.2/go.mod h1:WeDpKFqkjJU6RVUXL0M4MjLjNKQ5ZUWKmn1VOtbVplc= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.16.4 h1:xfkjR+oSH2FWuBqIuatV+ALSIjlX/0ro8JF3X5MzsXo= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.16.4/go.mod h1:WGVkzcLWQmGahUjgZDieFVFt07OSmaNMCW5pp+DLZYw= +github.com/aws/aws-sdk-go-v2/service/codedeploy v1.18.3 h1:QpPAopwlgH0ODSxpVabR+TmR/32NcD/KMEvhE5ohGsw= +github.com/aws/aws-sdk-go-v2/service/codedeploy v1.18.3/go.mod h1:XS0bHX1J7EFeoYPTXdg+GipSRP20erPm6WxTGj/xPoU= +github.com/aws/aws-sdk-go-v2/service/datapipeline v1.16.2 h1:c5+t2S4JCTBzj3/y9Z15TagZTK0vJFYvdNUYvk9UeAo= +github.com/aws/aws-sdk-go-v2/service/datapipeline v1.16.2/go.mod h1:RggMwt9IKmOq49+9okMMIGzoy/ZyEMZ23XmG8hq6oHs= +github.com/aws/aws-sdk-go-v2/service/docdb v1.24.0 h1:Oto0KebWdPEtGUA7IB2pizLWCeQ82NasO2mv/UcrwSk= +github.com/aws/aws-sdk-go-v2/service/docdb v1.24.0/go.mod h1:A7PqVcedUBaK/y89wiWysaEw+WqFPjgkcrxIZTOy/Yo= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.23.0 h1:xmSAn14nM6IdHyuWO/bsrAagOQtnqzuUCLxdVmj9nhg= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.23.0/go.mod h1:1HkLh8vaL4obF95fne7ZOu7sxomS/+vkBt3/+gqqwE4= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.127.0 h1:4xtATQuR0qIvX+QTWHlgTUnwlDPNzHcvMsB+qkRSPRo= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.127.0/go.mod h1:raUdIDoNuDPn9dMG3cCmIm8RoWOmZUqQPzuw8xpmB8Y= +github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 h1:y6LX9GUoEA3mO0qpFl1ZQHj1rFyPWVphlzebiSt2tKE= +github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2/go.mod h1:Q0LcmaN/Qr8+4aSBrdrXXePqoX0eOuYpJLbYpilmWnA= +github.com/aws/aws-sdk-go-v2/service/ecs v1.30.4 h1:j0VhL2v86gbsOKLQ1EDMhS2Lb0TROVIep7eFobc2Qq0= +github.com/aws/aws-sdk-go-v2/service/ecs v1.30.4/go.mod h1:1pSCxO2RQKwIg2ibxUcSmg9jbIZtfrXrVU72nY2jF3g= +github.com/aws/aws-sdk-go-v2/service/efs v1.21.9 h1:mlF3jj8aWvnd/o/KEJqzFUQEGqw2a7UOm1S4gzqWe/o= +github.com/aws/aws-sdk-go-v2/service/efs v1.21.9/go.mod h1:yXsn9Nd+xBewfLNGxu2HBjP8amz3/L6lGIsPZP1fhfc= +github.com/aws/aws-sdk-go-v2/service/eks v1.30.0 h1:d6WbsOHHsEMryKLc9oYCmvu4lrV9z9QLSQ5S44KSn0o= +github.com/aws/aws-sdk-go-v2/service/eks v1.30.0/go.mod h1:Nt5l6Vn68Hv0JWJ6dcQDKuBAKAfHUZSC9Ln8X/1fUMY= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.30.0 h1:isOi+s5w3lUupgMUnBd/Y1oZ04T7NG835vkQg75hnpo= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.30.0/go.mod h1:9fw8OlW99pw/i4qUkO7bNYYqznIPPuH/tsCHwa1ZYHc= +github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk v1.17.2 h1:a5T0ZZARvpJBSF3NTMG1Oh7MVrFqjaJrbTE7/Ro1iDM= +github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk v1.17.2/go.mod h1:RWRjHldUoeUDce3D6OIjCcIC1dJ5DeOHs0Y0inI2Xt8= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.18.0 h1:rw3OZB7wAdl0OA0w0dAy2kdsUSjUe4wSxNbu3rwi8zU= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.18.0/go.mod h1:T7I3S3o4CWa9sSV2DIK+5ZmuMXarrcrE8RYKGDaQ1I4= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.22.0 h1:DEdgH+R4MCPiuYW0G11pzU4U6kn+1WprM8N7gx1wnko= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.22.0/go.mod h1:/ZlJt5r04rRWDg/7K6cQ6Tq0ZUnUMVR2FRg0GGTy/e0= +github.com/aws/aws-sdk-go-v2/service/emr v1.29.0 h1:a+70mGT17GMYDek7hU4bDN8BE5zhd7JE5Dzth+0BR00= +github.com/aws/aws-sdk-go-v2/service/emr v1.29.0/go.mod h1:TUlGxOXunqrsVPdN98ergwcW9ZeR1+Fg0/qsQTho1ww= +github.com/aws/aws-sdk-go-v2/service/fsx v1.34.0 h1:P9vHaltsJyddWNcD8zl3ixyGxQjlY9PdfKtHunV5LQE= +github.com/aws/aws-sdk-go-v2/service/fsx v1.34.0/go.mod h1:EjIvHf1MOUWLXk5Gv2ot6BwlEyPHM4kceJ8IJv7KMME= +github.com/aws/aws-sdk-go-v2/service/glue v1.64.0 h1:fwarWmCqZ40V+CU/EsZ9r+f1RMVxqz3BVnwJtz9OUN0= +github.com/aws/aws-sdk-go-v2/service/glue v1.64.0/go.mod h1:U6H3cQ/25CnRt1rEsQ+Tk2q5Dnl3IXJZFHoRwc2WNE8= +github.com/aws/aws-sdk-go-v2/service/grafana v1.15.7 h1:6JIuHCZ1aBYbwqypQ1omjx0+cJh6iDVVY0lHnGdr3no= +github.com/aws/aws-sdk-go-v2/service/grafana v1.15.7/go.mod h1:L1GGWtZRULrM/5b4dtyakgcoPzIOMuxyNLSYcHA8SdU= +github.com/aws/aws-sdk-go-v2/service/iam v1.23.0 h1:rHRl/C8gMedaF3XEZ9SgXkdgEgd3ir9t6upZqSZ1F+8= +github.com/aws/aws-sdk-go-v2/service/iam v1.23.0/go.mod h1:d4c7P+mola/qBIgxgtVHK/w77vn+BlCsC/tbJ3m8m4Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15 h1:7R8uRYyXzdD71KWVCL78lJZltah6VVznXBazvKjfH58= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15/go.mod h1:26SQUPcTNgV1Tapwdt4a1rOsYRsnBsJHLMPoxK2b0d8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38 h1:skaFGzv+3kA+v2BPKhuekeb1Hbb105+44r8ASC+q5SE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38/go.mod h1:epIZoRSSbRIwLPJU5F+OldHhwZPBdpDeQkRdCeY3+00= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.37 h1:4LoizcvPT9A0tiAFhepxn0bGZXkzvN0pG0epydY3Pno= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.37/go.mod h1:7xBUZyP6LeLc+5Ym9PG7atqw4sR28sBtYcHETik+bPE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6 h1:9ulSU5ClouoPIYhDQdg9tpl83d5Yb91PXTKK+17q+ow= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6/go.mod h1:lnc2taBsR9nTlz9meD+lhFZZ9EWY712QHrRflWpTcOA= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.20.0 h1:OCYjSomi2Q8ttimk0DB4nNSAvoVOXfpSAwB0ZM4g1K0= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.20.0/go.mod h1:IKAdoalibJPPhb+riPJyKh9z/6V8n4J2X1yUto/W90Q= +github.com/aws/aws-sdk-go-v2/service/lambda v1.41.0 h1:AQK0v7JA0stOOi1cNOq4n9N3X1a3N3A2Ezt3UEyHL9o= +github.com/aws/aws-sdk-go-v2/service/lambda v1.41.0/go.mod h1:kFs07FNyTowZkz+dGBR33xJbzGs2mkC5Kfm6/lyR5CA= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.29.0 h1:Io+ezo97hWDmcg3tSMJbMoL1zd5zJ8BxK8Y8yB2Chfs= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.29.0/go.mod h1:u2yl3D9ygNXthorLQOj2wmMPSxWBGZ+XTlkVbsK2Bh8= +github.com/aws/aws-sdk-go-v2/service/mq v1.17.0 h1:SseTkq+6TtZv05ez/rBfQJQDueAa9DQosluNGF5ZHzo= +github.com/aws/aws-sdk-go-v2/service/mq v1.17.0/go.mod h1:GrFG5/W01G5Z2OXb+Qe40PjSzHQsXJTWSCNmvqAq6Rw= +github.com/aws/aws-sdk-go-v2/service/opensearch v1.22.0 h1:ATE++PFLW5V2CAujoI846MVD8MIFtrpSvCXdeXloY84= +github.com/aws/aws-sdk-go-v2/service/opensearch v1.22.0/go.mod h1:LPkOrJqBDqIzzNpnToYR79f5JQD/udkxyC9nECuHA3s= +github.com/aws/aws-sdk-go-v2/service/organizations v1.20.8 h1:FUd2lRsLCF+hKf7Ve9I10in/N0f+EVqZEXB/VZm8BZI= +github.com/aws/aws-sdk-go-v2/service/organizations v1.20.8/go.mod h1:0zR2FnFXmQBI+aHBNr6iQ9WuzssOkl7deBA+1c004Gk= +github.com/aws/aws-sdk-go-v2/service/ram v1.20.7 h1:afbEGsuDyeU+svV28+V3wozbHRoRNn5GSGxgXaUUjqU= +github.com/aws/aws-sdk-go-v2/service/ram v1.20.7/go.mod h1:hxfoKaeAQq9ybg0ZX+FK16nepP3+Ck7GmYrfBgPKcQE= +github.com/aws/aws-sdk-go-v2/service/rds v1.58.0 h1:ErFOugr4ORe+d6OGNk3Csr5ItF9DRcHJMFZnuh4isUM= +github.com/aws/aws-sdk-go-v2/service/rds v1.58.0/go.mod h1:NNx09yR8B7z4I5xTt2rUq+5h2lmA9T9bbm7NME/74Ac= +github.com/aws/aws-sdk-go-v2/service/redshift v1.31.0 h1:61PxIJHRpNVHypYy4+8WNx7q9izeMvw7C3+OLM4dZ4c= +github.com/aws/aws-sdk-go-v2/service/redshift v1.31.0/go.mod h1:WOT17e05/tPt7VV0Thz/sqtA1wE/C4iuxb2wsXyc5Qc= +github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.16.2 h1:03EH0RIXqyhwLZFjyJBG4hMTBw4rHMUtIESEwizxkg8= +github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.16.2/go.mod h1:qqGaDe21Tbh/b9vRv61ykdn/JM/xQr78SL4nYfRzh4E= +github.com/aws/aws-sdk-go-v2/service/route53 v1.31.0 h1:2u6uM0+E/bowkcCBe6KXhAFc5++tHuCZqJC3br1sI1c= +github.com/aws/aws-sdk-go-v2/service/route53 v1.31.0/go.mod h1:TQZBt/WaQy+zTHoW++rnl8JBrmZ0VO6EUbVua1+foCA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2 h1:Ll5/YVCOzRB+gxPqs2uD0R7/MyATC0w85626glSKmp4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2/go.mod h1:Zjfqt7KhQK+PO1bbOsFNzKgaq7TcxzmEoDWN8lM0qzQ= +github.com/aws/aws-sdk-go-v2/service/sagemaker v1.112.0 h1:ZN0GhpC0JEhWemCyXpUDEhpYaANwgs1Rr/Xot6mjHGo= +github.com/aws/aws-sdk-go-v2/service/sagemaker v1.112.0/go.mod h1:gbX8YRNFupoP6GuDH609e+QXejEPyQKrf/chjLTHYTk= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.6 h1:y3n83jEM6EuawrD5HZCh3eMj9RsfxniVLcXlyFMNITM= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.6/go.mod h1:A108ijf0IFtqhYApU+Gia80aPSAUfi9dItm+h5fWGJE= +github.com/aws/aws-sdk-go-v2/service/sfn v1.19.8 h1:qyewIfqnYEIvYx4SwH/0ObX6PpJ9SEEUUHmUQ3CNois= +github.com/aws/aws-sdk-go-v2/service/sfn v1.19.8/go.mod h1:rxuLnlLghUuQWgzHnqq2lXEwYmPnEFkX+RxC4QflXXs= +github.com/aws/aws-sdk-go-v2/service/sns v1.22.2 h1:zU+iUkj72bZFuIgUTCcAyVXs7Le1uX2LopHMnvZfn04= +github.com/aws/aws-sdk-go-v2/service/sns v1.22.2/go.mod h1:gLVePJ104BrkWKr4aU3CURZYZnZN7BQGDsB668Uh3ZY= +github.com/aws/aws-sdk-go-v2/service/sqs v1.24.7 h1:NZhGz9eHNTLPK9Bhq3wrRSUIu9BqcjWzC8UNK6MwUfI= +github.com/aws/aws-sdk-go-v2/service/sqs v1.24.7/go.mod h1:iWb2iGUERRXX3kEyKVtkjuMOW2YkDBcuhKCp5y37ys0= +github.com/aws/aws-sdk-go-v2/service/ssm v1.40.0 h1:DHZFzwbFXlfw15I0ERlTVB/YH9iHNr2C1axjRpB7/Gg= +github.com/aws/aws-sdk-go-v2/service/ssm v1.40.0/go.mod h1:qpnJ98BgJ3YUEvHMgJ1OADwaOgqhgv0nxnqAjTKupeY= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8= +github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/bishopfox/awsservicemap v1.0.2 h1:+NnuInVcQkja6y6h6eGseneU3m5Nzp9O++gR1s+FiyM= github.com/bishopfox/awsservicemap v1.0.2/go.mod h1:oy9Fyqh6AozQjShSx+zRNouTlp7k3z3YEMoFkN8rquc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -215,8 +234,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/dominikbraun/graph v0.22.2 h1:rFtrufgqXJy7daEMTxHIQWQc12Y1XHelXi2Heo1Bfho= -github.com/dominikbraun/graph v0.22.2/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= +github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= +github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -228,14 +247,16 @@ github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBD github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= -github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= +github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -290,8 +311,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -323,10 +344,10 @@ github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2px github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -351,8 +372,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -367,21 +388,19 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs= -go.mongodb.org/mongo-driver v1.11.7/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY= +go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= +go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -399,8 +418,8 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -469,8 +488,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= @@ -490,7 +509,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -534,13 +552,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 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= @@ -549,9 +567,10 @@ 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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= diff --git a/internal/aws.go b/internal/aws.go index 22b0442..c22d88f 100644 --- a/internal/aws.go +++ b/internal/aws.go @@ -126,8 +126,13 @@ func GetEnabledRegions(awsProfile string, version string) []string { // txtLogger - Returns the txt logger func TxtLogger() *logrus.Logger { + var txtFile *os.File + var err error txtLogger := logrus.New() - txtFile, err := os.OpenFile(fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath())), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + txtFile, err = os.OpenFile(fmt.Sprintf("%s/cloudfox-error.log", ptr.ToString(GetLogDirPath())), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + txtFile, err = os.OpenFile(fmt.Sprintf("./cloudfox-error.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + } if err != nil { panic(fmt.Sprintf("Failed to open log file %v", err)) } diff --git a/internal/aws/policy/condition.go b/internal/aws/policy/condition.go index f894f8f..8a5f09b 100644 --- a/internal/aws/policy/condition.go +++ b/internal/aws/policy/condition.go @@ -6,7 +6,7 @@ import ( ) // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html -// Conditions have the folling general structure: +// Conditions have the following general structure: // "Condition" : { "{condition-operator}" : { "{condition-key}" : "{condition-value}" }} type PolicyStatementCondition map[string]map[string]ListOrString @@ -20,7 +20,7 @@ func (psc *PolicyStatementCondition) IsEmpty() bool { // 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. +// Such policies should be reported to the user and analyzed case by case to judge if conditions are sufficiently restrictive. func (psc *PolicyStatementCondition) IsScopedOnAccountOrOrganization() bool { for condition, kv := range *psc { for k, v := range kv { diff --git a/internal/aws/policy/principal.go b/internal/aws/policy/principal.go index 2b66791..1679e54 100644 --- a/internal/aws/policy/principal.go +++ b/internal/aws/policy/principal.go @@ -3,6 +3,7 @@ package policy import ( "encoding/json" "errors" + "strings" ) type PolicyStatementPrincipal struct { @@ -92,3 +93,16 @@ func (ls *ListOrString) UnmarshalJSON(b []byte) error { return errors.New("not a string or list of strings") } + +// create a method on *PolicyStatementPrincipalObject that will determine if trusted principal is from the same account as the resource or a different account +func (pspo *PolicyStatementPrincipalObject) IsTrustedPrincipalSameAccount(accountID string) bool { + for _, principal := range pspo.CanonicalUser { + if strings.Contains(principal, ":") { + principalAccount := strings.Split(principal, ":")[4] + if principalAccount == accountID { + return true + } + } + } + return false +} diff --git a/internal/aws/policy/statement.go b/internal/aws/policy/statement.go index ef6058e..b68d97c 100644 --- a/internal/aws/policy/statement.go +++ b/internal/aws/policy/statement.go @@ -54,18 +54,6 @@ func (ps *PolicyStatement) GetAllActionsAsString() string { func (ps *PolicyStatement) GetAllPrincipalsAsString() string { principals := "" - // if len(ps.Principal.O.GetListOfPrincipals()) < 3 { - // for _, principal := range ps.Principal.O.GetListOfPrincipals() { - // principals = fmt.Sprintf("%s%s & ", principals, principal) - // } - // } else { - // principals = fmt.Sprintf("%s%d principals", principals, len(ps.Principal.O.GetListOfPrincipals())) - // } - // principals = strings.TrimSuffix(principals, " & ") - // principals = principals + "\n" - - // return principals - for _, principal := range ps.Principal.O.GetListOfPrincipals() { if len(ps.Principal.O.GetListOfPrincipals()) > 1 { principals = fmt.Sprintf("%s%s \n& ", principals, principal) @@ -133,3 +121,8 @@ func (ps *PolicyStatement) GetStatementSummaryInEnglish(caller string) string { return statementSummary } + +// GetResources as list of strings +func (ps *PolicyStatement) GetResources() []string { + return ps.Resource +} diff --git a/internal/aws_test.go b/internal/aws_test.go index 176c01a..8779623 100644 --- a/internal/aws_test.go +++ b/internal/aws_test.go @@ -71,7 +71,7 @@ func TestGetSelectedAWSProfiles(t *testing.T) { afero.WriteFile(UtilsFs, "/tmp/myfile.txt", test.fileData, 0755) output := GetSelectedAWSProfiles("/tmp/myfile.txt") if !compareSlice(output, test.expectedOutput) { - t.Errorf("Test Failed: %v inputted, %v expected, recieved: %v", test.fileData, test.expectedOutput, output) + t.Errorf("Test Failed: %v inputted, %v expected, received: %v", test.fileData, test.expectedOutput, output) } } } diff --git a/internal/azure.go b/internal/azure.go index d3ca0dd..b75d13e 100644 --- a/internal/azure.go +++ b/internal/azure.go @@ -49,6 +49,17 @@ func GetSubscriptionsClient() subscriptions.Client { return client } +func GetgraphRbacClient(tenantID string) graphrbac.DomainsClient { + client := graphrbac.NewDomainsClient(tenantID) + a, err := getAuthorizer(globals.AZ_GRAPH_ENDPOINT) + if err != nil { + log.Fatalf("failed to get azure active directory client: %s", err) + } + client.Authorizer = a + client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT) + return client +} + func GetResourceGroupsClient(subscriptionID string) resources.GroupsClient { client := resources.NewGroupsClient(subscriptionID) a, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT) diff --git a/internal/azure_test.go b/internal/azure_test.go index 730fb5e..0c03b3a 100644 --- a/internal/azure_test.go +++ b/internal/azure_test.go @@ -8,7 +8,7 @@ import ( "github.com/BishopFox/cloudfox/globals" ) -// Requires Az CLI Authentication to passs +// Requires Az CLI Authentication to pass func TestGetAuthorizer(t *testing.T) { t.Skip() subtests := []struct { diff --git a/internal/cache.go b/internal/cache.go index dc1090a..75c2e97 100644 --- a/internal/cache.go +++ b/internal/cache.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -37,7 +36,7 @@ func SaveCacheToFiles(directory string, accountID string) error { } filename := filepath.Join(directory, key+".json") - err = ioutil.WriteFile(filename, jsonData, 0644) + err = os.WriteFile(filename, jsonData, 0644) if err != nil { return err } @@ -55,7 +54,7 @@ func LoadCacheFromFiles(directory string) error { return err } - files, err := ioutil.ReadDir(directory) + files, err := os.ReadDir(directory) if err != nil { return err } @@ -66,7 +65,7 @@ func LoadCacheFromFiles(directory string) error { } filename := filepath.Join(directory, file.Name()) - jsonData, err := ioutil.ReadFile(filename) + jsonData, err := os.ReadFile(filename) if err != nil { return err } @@ -133,7 +132,8 @@ func LoadCacheFromGobFiles(directory string) error { return err } - files, err := ioutil.ReadDir(directory) + files, err := os.ReadDir(directory) + if err != nil { return err } @@ -142,6 +142,10 @@ func LoadCacheFromGobFiles(directory string) error { if file.IsDir() { continue } + // if the filetype is json, skip it + if filepath.Ext(file.Name()) == ".json" { + continue + } filename := filepath.Join(directory, file.Name()) file, err := os.Open(filename) diff --git a/internal/log.go b/internal/log.go index fac83c3..b27d366 100644 --- a/internal/log.go +++ b/internal/log.go @@ -23,7 +23,9 @@ func GetLogDirPath() *string { if _, err := os.Stat(dir); os.IsNotExist(err) { err = os.MkdirAll(dir, 0700) if err != nil { - log.Fatalf("[-] Failed to read or create cloudfox directory") + log.Printf("[-] Failed to read or create cloudfox directory") + dir, err = os.Getwd() + return ptr.String(dir) } } return ptr.String(dir) diff --git a/internal/output.go b/internal/output.go index 837be6c..6215898 100644 --- a/internal/output.go +++ b/internal/output.go @@ -15,7 +15,7 @@ import ( "golang.org/x/crypto/ssh/terminal" ) -// This struct is here to mantain compatibility with legacy cloudfox code +// This struct is here to maintain compatibility with legacy cloudfox code type OutputData2 struct { Headers []string Body [][]string diff --git a/internal/output2.go b/internal/output2.go index fad9d67..a0cad52 100644 --- a/internal/output2.go +++ b/internal/output2.go @@ -8,6 +8,7 @@ import ( "os" "path" "regexp" + "strings" "github.com/aquasecurity/table" "github.com/fatih/color" @@ -41,6 +42,7 @@ type TableFile struct { TableFilePointer afero.File CSVFilePointer afero.File JSONFilePointer afero.File + TableCols []string Header []string Body [][]string } @@ -179,20 +181,10 @@ func (l *LootClient) writeLootFiles() []string { func (b *TableClient) printTablesToScreen(tableFiles []TableFile) { for _, tf := range tableFiles { + tf.Body, tf.Header = adjustBodyForTable(tf.TableCols, tf.Header, tf.Body) 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) - // } if !b.Wrap { t.SetColumnMaxWidth(standardColumnWidth) } @@ -245,6 +237,7 @@ func (b *TableClient) writeTableFiles(files []TableFile) []string { var fullFilePaths []string for _, file := range b.TableFiles { + file.Body, file.Header = adjustBodyForTable(file.TableCols, file.Header, file.Body) standardColumnWidth := 1000 t := table.New(file.TableFilePointer) @@ -305,6 +298,8 @@ func (b *TableClient) writeCSVFiles() []string { csvWriter.Write(file.Header) for _, row := range file.Body { row = removeColorCodesFromSlice(row) + //row = removeNewLinesFromSlice(row) + csvWriter.Write(row) } csvWriter.Flush() @@ -316,6 +311,39 @@ func (b *TableClient) writeCSVFiles() []string { return fullFilePaths } +// replace newlines in row to make them csv and json safe +func removeNewLinesFromNestedSlice(input [][]string) [][]string { + // Regular expression to match new lines + newLineRegExp := regexp.MustCompile(`\n`) + + // Create a new slice to store the slices with new lines removed + noNewLineNestedSlice := make([][]string, len(input)) + + for i, strSlice := range input { + noNewLineNestedSlice[i] = make([]string, len(strSlice)) + for j, str := range strSlice { + noNewLineNestedSlice[i][j] = newLineRegExp.ReplaceAllString(str, "") + } + } + + return noNewLineNestedSlice +} + +// replace newlines in slice of strings to make them render as newlines in csv and json when opened in excel +func removeNewLinesFromSlice(input []string) []string { + // Regular expression to match new lines + newLineRegExp := regexp.MustCompile(`\n`) + + // Create a new slice to store the strings with new lines removed + noNewLineSlice := make([]string, len(input)) + + for i, str := range input { + noNewLineSlice[i] = newLineRegExp.ReplaceAllString(str, " \\n") + } + + return noNewLineSlice +} + func (b *TableClient) createJSONFiles() { for i, file := range b.TableFiles { if b.DirectoryName == "" { @@ -397,3 +425,33 @@ func (b *TableClient) writeJSONFiles() []string { return fullFilePaths } + +func adjustBodyForTable(tableHeaders []string, fullHeaders []string, fullBody [][]string) ([][]string, []string) { + if tableHeaders == nil || len(tableHeaders) == 0 { + return fullBody, fullHeaders + } + + columnIndices := make([]int, 0) + selectedHeaders := make([]string, 0) + + for _, tableHeader := range tableHeaders { + for j, fullHeader := range fullHeaders { + if strings.ToLower(tableHeader) == strings.ToLower(fullHeader) { + columnIndices = append(columnIndices, j) + selectedHeaders = append(selectedHeaders, fullHeader) + break + } + } + } + + adjustedBody := make([][]string, len(fullBody)) + for i, row := range fullBody { + newRow := make([]string, len(columnIndices)) + for k, index := range columnIndices { + newRow[k] = row[index] + } + adjustedBody[i] = newRow + } + + return adjustedBody, selectedHeaders +} diff --git a/main.go b/main.go index 0e022e1..ae6b6da 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,7 @@ import ( var ( rootCmd = &cobra.Command{ Use: os.Args[0], - Version: "1.11.2", + Version: "1.13.0-prerelease", } )