mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-07 17:05:53 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e63b0e18 | ||
|
|
eaac503bff | ||
|
|
cec0d49602 | ||
|
|
6a21b9665d | ||
|
|
a1903f67f7 | ||
|
|
8d039323bf | ||
|
|
fd01647a8a | ||
|
|
f415f991c8 | ||
|
|
15e0507ad6 | ||
|
|
80c00f3d97 | ||
|
|
59afbb381e | ||
|
|
ca437d3626 | ||
|
|
1ca5dbfb10 | ||
|
|
f00044670b | ||
|
|
ae1dac6c21 | ||
|
|
237b0735a8 | ||
|
|
18e38bf1ea | ||
|
|
b5908fc720 | ||
|
|
29514a96b5 | ||
|
|
b2c450ee02 | ||
|
|
a65610390b | ||
|
|
997c13d4de | ||
|
|
1d1d198a01 | ||
|
|
6069267758 | ||
|
|
e08967bb31 | ||
|
|
8e99347789 | ||
|
|
ff810ba17d | ||
|
|
23fc346e4f | ||
|
|
89789a05c1 | ||
|
|
5827abb2a2 | ||
|
|
13a03c5de2 | ||
|
|
dd6bfefa9d | ||
|
|
8c4bcc035b | ||
|
|
a38451c1cd | ||
|
|
5df1ca6d75 | ||
|
|
ea2ee3dd2c | ||
|
|
97e2fef388 | ||
|
|
1d53b984ac | ||
|
|
525f262460 | ||
|
|
f13df0708c |
No files matched your search
@@ -15,6 +15,7 @@
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
.vscode
|
||||
.idea
|
||||
launch.json
|
||||
|
||||
######################################################
|
||||
|
||||
@@ -14,6 +14,7 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments.
|
||||
|
||||
## Demos, Examples, Walkthroughs
|
||||
* [Blog - Introducing: CloudFox](https://bishopfox.com/blog/introducing-cloudfox)
|
||||
* [Video - CloudFox + CloudFoxable A Powerful Duo for Mastering the Art of Identifying and Exploiting AWS Attack Paths](https://www.youtube.com/watch?v=RdQiIvCrSzk)
|
||||
* [Video - Penetration Testing with CloudFox](https://www.youtube.com/watch?v=Ljt_JUp5HbM)
|
||||
* [Video - CloudFox Intro Demos](https://www.youtube.com/watch?v=ReWoUgpUuiQ)
|
||||
* [Video - Tool Talk: CloudFox AWS sub-command walkthroughs](https://youtu.be/KKsYfL5uVU4?t=360)
|
||||
@@ -21,6 +22,10 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments.
|
||||
## Intentionally Vulnerable Playground
|
||||
* [CloudFoxable - A Gamified Cloud Hacking Sandbox](https://cloudfoxable.bishopfox.com/)
|
||||
|
||||
## Want to chat about CloudFox?
|
||||
Join us on the [RedSec discord server](https://discord.gg/redsec)
|
||||
|
||||
|
||||
## Quick Start
|
||||
CloudFox is modular (you can run one command at a time), but there is an `aws all-checks` command that will run the other aws commands for you with sane defaults:
|
||||
|
||||
@@ -43,7 +48,7 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
|
||||
|
||||
| Provider| CloudFox Commands |
|
||||
| - | - |
|
||||
| AWS | 30 |
|
||||
| AWS | 33 |
|
||||
| Azure | 4 |
|
||||
| GCP | Support Planned |
|
||||
| Kubernetes | Support Planned |
|
||||
@@ -62,13 +67,20 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
|
||||
[Install Go](https://golang.org/doc/install), clone the CloudFox repository and compile from source
|
||||
```
|
||||
# git clone https://github.com/BishopFox/cloudfox.git
|
||||
...omitted for brevity...
|
||||
# cd ./cloudfox
|
||||
# Make any changes necessary
|
||||
# go build .
|
||||
# ./cloudfox
|
||||
```
|
||||
|
||||
**Option 5:** Testing a bug fix
|
||||
```
|
||||
git clone [email protected]:BishopFox/cloudfox.git
|
||||
git checkout seth-dev
|
||||
go build .
|
||||
./cloudfox [rest of the command options]
|
||||
```
|
||||
|
||||
# Prerequisites
|
||||
|
||||
|
||||
@@ -97,8 +109,11 @@ Additional policy notes (as of 09/2022):
|
||||
| - | - | - |
|
||||
| AWS | [all-checks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#all-checks) | Run all of the other commands using reasonable defaults. You'll still want to check out the non-default options of each command, but this is a great place to start. |
|
||||
| AWS | [access-keys](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#access-keys) | Lists active access keys for all users. Useful for cross referencing a key you found with which in-scope account it belongs to. |
|
||||
| AWS | [api-gw](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#api-gw) | Lists API gateway endpoints and gives you custom curl commands including API tokens if they are stored in metadata. |
|
||||
| AWS | [buckets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#filesystems) | Lists the buckets in the account and gives you handy commands for inspecting them further. |
|
||||
| AWS | [cloudformation](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#cloudformation) | Lists the cloudformation stacks in the account. Generates loot file with stack details, stack parameters, and stack output - look for secrets. |
|
||||
| AWS | [codebuild](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#codebuild) | Enumerate CodeBuild projects |
|
||||
| AWS | [databases](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#databases) | Enumerate RDS databases. Get a loot file with connection strings. |
|
||||
| AWS | [ecr](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ecr) | List the most recently pushed image URI from all repositories. Use the loot file to pull selected images down with docker/nerdctl for inspection. |
|
||||
| AWS | [ecs-tasks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ecs-tasks) | List all ecs tasks. This returns a list of ecs tasks and associated cluster, task definition, container instance, launch type, and associated IAM principal. |
|
||||
| AWS | [eks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#eks) | List all EKS clusters, see if they expose their endpoint publicly, and check the associated IAM roles attached to reach cluster or node group. Generates a loot file with the `aws eks udpate-kubeconfig` command needed to connect to each cluster. |
|
||||
@@ -111,27 +126,30 @@ Additional policy notes (as of 09/2022):
|
||||
| AWS | [inventory](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#inventory) | Gain a rough understanding of size of the account and preferred regions. |
|
||||
| AWS | [lambda](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#lambda) | Lists the lambda functions in the account, including which one's have admin roles attached. Also gives you handy commands for downloading each function. |
|
||||
| AWS | [network-ports](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#network-ports) | Enumerates AWS services that are potentially exposing a network service. The security groups and the network ACLs are parsed for each resource to determine what ports are potentially exposed. |
|
||||
| AWS | [outbound-assumed-roles](#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
|
||||
| AWS | [orgs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#orgs) | Enumerate accounts in an organization |
|
||||
| AWS | [outbound-assumed-roles](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
|
||||
| AWS | [permissions](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#permissions) | Enumerates IAM permissions associated with all users and roles. Grep this output to figure out what permissions a particular principal has rather than logging into the AWS console and painstakingly expanding each policy attached to the principal you are investigating. |
|
||||
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
|
||||
| AWS | [pmapper](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#pmapper) | Looks for pmapper data stored on the local filesystem, [in the locations defined here](https://github.com/nccgroup/PMapper/wiki/Frequently-Asked-Questions#where-does-pmapper-store-its-data). If pmapper data has been found (you already ran `pmapper graph create`), then this command will use this data to build a graph in cloudfox memory let you know who can privesc to admin.
|
||||
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
|
||||
| AWS | [ram](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ram) | List all resources in this account that are shared with other accounts, or resources from other accounts that are shared with this account. Useful for cross-account attack paths. |
|
||||
| AWS | [resource-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#resource-trusts) | Looks through multiple services that support resource policies and helps you find any overly permissive resource trusts.|
|
||||
| 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. |
|
||||
| AWS | [workloads](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#workloads) | List all of the compute workloads and what role they have. Tells you if any of the roles are admin (bad) and if you have pmapper data locally, it will tell you if any of the roles can privesc to admin (also bad) |
|
||||
|
||||
|
||||
# Azure Commands
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | [whoami](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#whoami) | Displays information on the tenant, subscriptions and resource groups available to your current Azure CLI session. This is useful to provide situation awareness on what tenant and subscription IDs to use with the other sub commands. |
|
||||
| Azure | [instances](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#instances) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
|
||||
| Azure | [inventory](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#inventory) | Display an inventory table of all resources per location. |
|
||||
| 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 |
|
||||
| Azure | [vms](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#vms) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
|
||||
|
||||
# Authors
|
||||
* [Carlos Vendramini](https://github.com/carlosvendramini-bf)
|
||||
|
||||
+24
-23
@@ -2,7 +2,6 @@ package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -12,9 +11,7 @@ import (
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigateway"
|
||||
apigatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
|
||||
apigatewayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
@@ -25,8 +22,8 @@ var CURL_COMMAND string = "curl -X %s %s"
|
||||
|
||||
type ApiGwModule struct {
|
||||
// General configuration data
|
||||
APIGatewayClient *apigateway.Client
|
||||
APIGatewayv2Client *apigatewayv2.Client
|
||||
APIGatewayClient sdk.APIGatewayClientInterface
|
||||
APIGatewayv2Client sdk.APIGatewayv2ClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -127,13 +124,17 @@ func (m *ApiGwModule) PrintApiGws(outputDirectory string, verbosity int) {
|
||||
|
||||
}
|
||||
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)))
|
||||
filepath := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
Wrap: m.WrapTable,
|
||||
DirectoryName: filepath,
|
||||
},
|
||||
Loot: internal.LootClient{
|
||||
DirectoryName: filepath,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
@@ -142,9 +143,13 @@ func (m *ApiGwModule) PrintApiGws(outputDirectory string, verbosity int) {
|
||||
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)
|
||||
loot := m.writeLoot(filepath, verbosity)
|
||||
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
|
||||
Name: m.output.CallingModule,
|
||||
Contents: loot,
|
||||
})
|
||||
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
|
||||
|
||||
fmt.Printf("[%s][%s] %s API gateways found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No API gateways found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
@@ -199,14 +204,8 @@ func (m *ApiGwModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) string {
|
||||
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, "api-gws.txt")
|
||||
|
||||
var out string
|
||||
@@ -237,12 +236,12 @@ func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
out += line + "\n"
|
||||
}
|
||||
|
||||
err = os.WriteFile(f, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
panic(err.Error())
|
||||
}
|
||||
// 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()
|
||||
@@ -253,6 +252,8 @@ func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
|
||||
return out
|
||||
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestApiGw(t *testing.T) {
|
||||
|
||||
m := ApiGwModule{
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-east-1"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
WrapTable: false,
|
||||
APIGatewayClient: &sdk.MockedAWSAPIGatewayClient{},
|
||||
APIGatewayv2Client: &sdk.MockedAWSAPIGatewayv2Client{},
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
//tmpDir := "~/.cloudfox/"
|
||||
tmpDir := ptr.ToString(internal.GetLogDirPath())
|
||||
|
||||
m.PrintApiGws(tmpDir, 2)
|
||||
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/api-gw.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
//print the results file to the screen
|
||||
//fmt.Println(string(resultsFile))
|
||||
|
||||
// I want a test that runs the main function and checks the output to see if the following items are in the output: "https://qwerty.execute-api.us-east-1.amazonaws.com/stage1/path1", "https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage1/route2"
|
||||
|
||||
expectedResults := []string{
|
||||
"https://qwerty.execute-api.us-east-1.amazonaws.com/stage1/path1",
|
||||
"https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage1/route2",
|
||||
"https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage2/route1",
|
||||
"23oieuwefo3rfs",
|
||||
}
|
||||
|
||||
for _, expected := range expectedResults {
|
||||
if !strings.Contains(string(resultsFile), expected) {
|
||||
t.Errorf("Expected %s to be in the results file", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
func initIAMSimClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
|
||||
|
||||
iamSimMod := IamSimulatorModule{
|
||||
IAMClient: iamSimPPClient,
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
IAMClient: iamSimPPClient,
|
||||
Caller: caller,
|
||||
AWSProfileProvided: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
}
|
||||
|
||||
return iamSimMod
|
||||
|
||||
+221
-94
@@ -3,7 +3,6 @@ package aws
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -23,6 +22,7 @@ type DatabasesModule struct {
|
||||
RedshiftClient sdk.AWSRedShiftClientInterface
|
||||
DynamoDBClient sdk.DynamoDBClientInterface
|
||||
DocDBClient sdk.DocDBClientInterface
|
||||
NeptuneClient sdk.NeptuneClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
@@ -53,6 +53,7 @@ type Database struct {
|
||||
Protocol string
|
||||
Public string
|
||||
Size string
|
||||
Roles string
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int) {
|
||||
@@ -68,13 +69,13 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
|
||||
}
|
||||
|
||||
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))
|
||||
fmt.Printf("[%s][%s] Supported Services: RDS, Redshift, DynamoDB, DocumentDB, Neptune\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
|
||||
//fire up the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
@@ -112,7 +113,8 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
|
||||
"Size",
|
||||
"UserName",
|
||||
"Endpoint",
|
||||
//"Port",
|
||||
"Port",
|
||||
"Roles",
|
||||
//"Protocol",
|
||||
//"Public",
|
||||
|
||||
@@ -139,6 +141,8 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
|
||||
"Size",
|
||||
"UserName",
|
||||
"Endpoint",
|
||||
"Port",
|
||||
"Roles",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
@@ -150,6 +154,7 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
|
||||
"Size",
|
||||
"UserName",
|
||||
"Endpoint",
|
||||
"Port",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +171,8 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
|
||||
m.Databases[i].Size,
|
||||
m.Databases[i].UserName,
|
||||
m.Databases[i].Endpoint,
|
||||
// strconv.Itoa(int(m.Databases[i].Port)),
|
||||
strconv.Itoa(int(m.Databases[i].Port)),
|
||||
m.Databases[i].Roles,
|
||||
// m.Databases[i].Protocol,
|
||||
// m.Databases[i].Public,
|
||||
},
|
||||
@@ -174,13 +180,17 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
|
||||
|
||||
}
|
||||
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)))
|
||||
filepath := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
Wrap: m.WrapTable,
|
||||
DirectoryName: filepath,
|
||||
},
|
||||
Loot: internal.LootClient{
|
||||
DirectoryName: filepath,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
@@ -190,9 +200,15 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
|
||||
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)
|
||||
loot := m.writeLoot(filepath, verbosity)
|
||||
//o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
|
||||
Name: "databases-UrlsOnly.txt",
|
||||
Contents: loot,
|
||||
})
|
||||
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
|
||||
|
||||
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))
|
||||
@@ -217,56 +233,100 @@ func (m *DatabasesModule) Receiver(receiver chan Database, receiverDone chan boo
|
||||
func (m *DatabasesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
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)
|
||||
}
|
||||
|
||||
m.executeRdsCheck(r, wg, semaphore, dataReceiver, serviceMap) // Also returns Neptune and DocDB
|
||||
m.executeRedshiftCheck(r, wg, semaphore, dataReceiver, serviceMap)
|
||||
m.executeDynamoDbCheck(r, wg, semaphore, dataReceiver, serviceMap)
|
||||
//m.executeDocDbCheck(r, wg, semaphore, dataReceiver, serviceMap)
|
||||
//m.executeNeptuneCheck(r, wg, semaphore, dataReceiver, serviceMap)
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
type check struct {
|
||||
region string
|
||||
wg *sync.WaitGroup
|
||||
semaphore chan struct{}
|
||||
dataReceiver chan Database
|
||||
serviceMap *awsservicemap.AwsServiceMap
|
||||
service string
|
||||
executor func(string, *sync.WaitGroup, chan struct{}, chan Database)
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeCheck(check check) {
|
||||
res, err := check.serviceMap.IsServiceInRegion(check.service, check.region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
panic(err.Error())
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
check.wg.Add(1)
|
||||
go check.executor(check.region, check.wg, check.semaphore, check.dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeRdsCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "rds",
|
||||
executor: m.getRdsClustersPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeRedshiftCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "redshift",
|
||||
executor: m.getRedshiftDatabasesPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeDynamoDbCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "dynamodb",
|
||||
executor: m.getDynamoDBTablesPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeDocDbCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "docdb",
|
||||
executor: m.getDocDBTablesPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeNeptuneCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "neptune",
|
||||
executor: m.getNeptuneDatabasesPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) string {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
f := filepath.Join(path, "databases-UrlsOnly.txt")
|
||||
|
||||
var out string
|
||||
@@ -275,12 +335,12 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
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())
|
||||
}
|
||||
// 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()
|
||||
@@ -291,6 +351,8 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
|
||||
return out
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
@@ -307,45 +369,58 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
|
||||
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
|
||||
DBClusters, err := sdk.CachedRDSDescribeDBClusters(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 aws.ToBool(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: aws.ToInt32(port),
|
||||
Protocol: aws.ToString(instance.Engine),
|
||||
Public: public,
|
||||
}
|
||||
for _, cluster := range DBClusters {
|
||||
var public string
|
||||
var service string
|
||||
var roles string
|
||||
if cluster.Endpoint == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
name := aws.ToString(cluster.DBClusterIdentifier)
|
||||
port := cluster.Port
|
||||
endpoint := aws.ToString(cluster.Endpoint)
|
||||
engine := aws.ToString(cluster.Engine)
|
||||
|
||||
if aws.ToBool(cluster.PubliclyAccessible) {
|
||||
public = "True"
|
||||
} else {
|
||||
public = "False"
|
||||
}
|
||||
|
||||
if isNeptune(cluster.Engine) {
|
||||
service = "Neptune"
|
||||
} else if isDocDB(cluster.Engine) {
|
||||
service = "DocsDB"
|
||||
} else {
|
||||
service = "RDS"
|
||||
}
|
||||
|
||||
associatedRoles := cluster.AssociatedRoles
|
||||
for _, role := range associatedRoles {
|
||||
roles = roles + aws.ToString(role.RoleArn) + " "
|
||||
}
|
||||
|
||||
dataReceiver <- Database{
|
||||
AWSService: service,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Engine: engine,
|
||||
Endpoint: endpoint,
|
||||
UserName: aws.ToString(cluster.MasterUsername),
|
||||
Port: aws.ToInt32(port),
|
||||
Protocol: aws.ToString(cluster.Engine),
|
||||
Public: public,
|
||||
Roles: roles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
@@ -369,7 +444,7 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG
|
||||
|
||||
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.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++
|
||||
@@ -381,19 +456,19 @@ func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitG
|
||||
name := aws.ToString(cluster.DBName)
|
||||
//id := workspace.Id
|
||||
endpoint := aws.ToString(cluster.Endpoint.Address)
|
||||
port := aws.ToInt32(cluster.Endpoint.Port)
|
||||
|
||||
if aws.ToBool(cluster.PubliclyAccessible) {
|
||||
public = "True"
|
||||
} else {
|
||||
public = "False"
|
||||
}
|
||||
port := cluster.Endpoint.Port
|
||||
dataReceiver <- Database{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Port: aws.ToInt32(port),
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
Public: public,
|
||||
}
|
||||
@@ -442,7 +517,6 @@ func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGrou
|
||||
Endpoint: "N/A",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getDocDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
@@ -487,3 +561,56 @@ func (m *DatabasesModule) getDocDBTablesPerRegion(r string, wg *sync.WaitGroup,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getNeptuneDatabasesPerRegion(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++
|
||||
|
||||
clusters, err := sdk.CachedNeptuneDescribeDBClusters(m.NeptuneClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, cluster := range clusters {
|
||||
if !isNeptune(cluster.Engine) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := aws.ToString(cluster.DBClusterIdentifier)
|
||||
|
||||
endpoint := aws.ToString(cluster.Endpoint)
|
||||
port := aws.ToInt32(cluster.Port)
|
||||
userName := aws.ToString(cluster.MasterUsername)
|
||||
engine := aws.ToString(cluster.Engine)
|
||||
|
||||
dataReceiver <- Database{
|
||||
AWSService: "Neptune",
|
||||
Region: r,
|
||||
Name: name,
|
||||
Engine: engine,
|
||||
Endpoint: endpoint,
|
||||
Port: port,
|
||||
UserName: userName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isNeptune(engine *string) bool {
|
||||
return *engine == "neptune"
|
||||
}
|
||||
|
||||
func isDocDB(engine *string) bool {
|
||||
return *engine == "docdb"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestDatabasesCommand(t *testing.T) {
|
||||
|
||||
m := DatabasesModule{
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-east-1"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
WrapTable: false,
|
||||
RDSClient: &sdk.MockedRDSClient{},
|
||||
NeptuneClient: &sdk.MockedNeptuneClient{},
|
||||
DynamoDBClient: &sdk.MockedAWSDynamoDBClient{},
|
||||
RedshiftClient: &sdk.MockedRedshiftClient{},
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := ptr.ToString(internal.GetLogDirPath())
|
||||
|
||||
m.PrintDatabases(tmpDir, 2)
|
||||
//resultsFile, err := afero.ReadFile(fs, "table/databases.txt")
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/databases.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFile, err)
|
||||
}
|
||||
|
||||
expectedResults := []string{
|
||||
"db1.cluster-123456789012.us-west-2.rds.amazonaws.com",
|
||||
"db2.cluster-123456789012.us-west-2.rds.amazonaws.com",
|
||||
"db3.cluster-123456789012.us-west-2.neptune.amazonaws.com",
|
||||
"db4.cluster-123456789012.us-west-2.docdb.amazonaws.com",
|
||||
}
|
||||
|
||||
for _, expected := range expectedResults {
|
||||
if !strings.Contains(string(resultsFile), expected) {
|
||||
t.Errorf("Expected %s to be in the results file", expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -836,7 +836,7 @@ func (m *EnvsModule) interestingEnvVarsOnly() [][]string {
|
||||
for _, envVar := range m.EnvironmentVariables {
|
||||
if envVar.interesting {
|
||||
interestingBody = append(
|
||||
m.output.Body, []string{
|
||||
interestingBody, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
envVar.service,
|
||||
envVar.region,
|
||||
|
||||
+38
-22
@@ -27,9 +27,10 @@ type IamSimulatorModule struct {
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
Goroutines int
|
||||
AWSProfileProvided string
|
||||
AWSProfileStub string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
SimulatorResults []SimulatorResult
|
||||
@@ -76,16 +77,19 @@ 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", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
|
||||
var filename string
|
||||
var actionList []string
|
||||
var pmapperCommands []string
|
||||
var pmapperOutFileName string
|
||||
var inputArn string
|
||||
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
if m.AWSProfileProvided == "" {
|
||||
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
|
||||
} else {
|
||||
m.AWSProfileStub = m.AWSProfileProvided
|
||||
}
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
@@ -104,7 +108,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
if principal != "" {
|
||||
if action != "" {
|
||||
// The user specified a specific --principal and a specific --action
|
||||
fmt.Printf("[%s][%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), principal, action)
|
||||
fmt.Printf("[%s][%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), principal, action)
|
||||
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
actionList = append(actionList, action)
|
||||
// if user supplied a principal name without the arn, try to create the arn as a user and as a role and run both
|
||||
@@ -122,7 +126,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
|
||||
} else {
|
||||
// The user specified a specific --principal, but --action was empty
|
||||
fmt.Printf("[%s][%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), principal)
|
||||
fmt.Printf("[%s][%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), principal)
|
||||
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
|
||||
// if user supplied a principal name without the arn, try to create the arn as a user and as a role and run both
|
||||
@@ -142,7 +146,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
} else {
|
||||
if action != "" {
|
||||
// The did not specify a specific --principal, but they did specify an --action
|
||||
fmt.Printf("[%s][%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), action)
|
||||
fmt.Printf("[%s][%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), action)
|
||||
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
actionList = append(actionList, action)
|
||||
wg.Add(1)
|
||||
@@ -150,15 +154,23 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
wg.Add(1)
|
||||
m.getIAMRoles(wg, actionList, resource, dataReceiver)
|
||||
pmapperOutFileName = filepath.Join(filename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
|
||||
if m.AWSProfileProvided != "" {
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfileProvided, action, resource, pmapperOutFileName))
|
||||
} else {
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper query \"who can do %s with %s\" | tee %s\n", action, resource, pmapperOutFileName))
|
||||
}
|
||||
} else {
|
||||
// Both --principal and --action are empty. Run in default mode!
|
||||
fmt.Printf("[%s][%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
|
||||
filename = m.output.CallingModule
|
||||
m.executeChecks(wg, resource, dataReceiver)
|
||||
for _, action := range defaultActionNames {
|
||||
pmapperOutFileName = filepath.Join(filename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
|
||||
if m.AWSProfileProvided != "" {
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfileProvided, action, resource, pmapperOutFileName))
|
||||
} else {
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper query \"who can do %s with %s\" | tee %s\n", action, resource, pmapperOutFileName))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -225,7 +237,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
@@ -240,10 +252,10 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
TableCols: tableCols,
|
||||
Name: filename,
|
||||
})
|
||||
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.PrefixIdentifier = m.AWSProfileStub
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, 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][%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.AWSProfileStub))
|
||||
// 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)
|
||||
@@ -251,9 +263,9 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
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))
|
||||
fmt.Printf("[%s][%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
|
||||
}
|
||||
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)
|
||||
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.AWSProfileStub), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pmapperCommands []string) {
|
||||
@@ -266,7 +278,11 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
|
||||
|
||||
outFile := filepath.Join(path, "iam-simulator-pmapper-commands.txt")
|
||||
var out string
|
||||
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfile)
|
||||
if m.AWSProfileProvided != "" {
|
||||
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfileProvided)
|
||||
} else {
|
||||
out = out + fmt.Sprintf("pmapper graph create\n")
|
||||
}
|
||||
for _, line := range pmapperCommands {
|
||||
out = out + line
|
||||
}
|
||||
@@ -278,12 +294,12 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), outFile)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), outFile)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -512,6 +512,21 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
|
||||
|
||||
if instance.IamInstanceProfile == nil {
|
||||
profile = "NoInstanceProfile"
|
||||
dataReceiver <- MappedInstance{
|
||||
ID: aws.ToString(instance.InstanceId),
|
||||
Name: aws.ToString(&name),
|
||||
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)),
|
||||
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
|
||||
State: string(instance.State.Name),
|
||||
ExternalIP: externalIP,
|
||||
PrivateIP: aws.ToString(instance.PrivateIpAddress),
|
||||
Profile: profile,
|
||||
Role: "",
|
||||
Region: region,
|
||||
Admin: adminRole,
|
||||
CanPrivEsc: "",
|
||||
}
|
||||
|
||||
} else {
|
||||
profileArn = aws.ToString(instance.IamInstanceProfile.Arn)
|
||||
|
||||
|
||||
+34
-30
@@ -20,16 +20,17 @@ import (
|
||||
|
||||
type ResourceTrustsModule struct {
|
||||
// General configuration data
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
AWSMFAToken string
|
||||
|
||||
AWSProfile string
|
||||
CloudFoxVersion string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
AWSMFAToken string
|
||||
AWSConfig aws.Config
|
||||
AWSProfileProvided string
|
||||
AWSProfileStub string
|
||||
CloudFoxVersion string
|
||||
|
||||
Resources2 []Resource2
|
||||
CommandCounter internal.CommandCounter
|
||||
@@ -62,13 +63,16 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
|
||||
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, Glue, Lambda, SecretsManager, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
if m.AWSProfileProvided == "" {
|
||||
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
|
||||
} else {
|
||||
m.AWSProfileStub = m.AWSProfileProvided
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileProvided, aws.ToString(m.Caller.Account)))
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating Resources with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: CodeBuild, ECR, EFS, Glue, Lambda, SecretsManager, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
@@ -172,16 +176,16 @@ func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.PrefixIdentifier = m.AWSProfileStub
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, 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] %s resource policies found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), 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] No resource policies found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
|
||||
}
|
||||
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)
|
||||
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.AWSProfileStub), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
@@ -292,7 +296,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.WrapTable, m.AWSMFAToken)
|
||||
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.WrapTable, m.AWSMFAToken)
|
||||
|
||||
ListTopics, err := cloudFoxSNSClient.listTopics(r)
|
||||
if err != nil {
|
||||
@@ -368,7 +372,7 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.AWSMFAToken)
|
||||
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.AWSMFAToken)
|
||||
|
||||
ListBuckets, err := sdk.CachedListBuckets(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
@@ -451,7 +455,7 @@ func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGrou
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListQueues, err := cloudFoxSQSClient.listQueues(r)
|
||||
if err != nil {
|
||||
@@ -513,7 +517,7 @@ func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGro
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
DescribeRepositories, err := sdk.CachedECRDescribeRepositories(cloudFoxECRClient.ECRClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
@@ -577,7 +581,7 @@ func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, w
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListProjects, err := sdk.CachedCodeBuildListProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r)
|
||||
if err != nil {
|
||||
@@ -651,7 +655,7 @@ func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitG
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListFunctions, err := cloudFoxLambdaClient.listFunctions(r)
|
||||
if err != nil {
|
||||
@@ -716,7 +720,7 @@ func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *s
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListFileSystems, err := sdk.CachedDescribeFileSystems(cloudFoxEFSClient.EFSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
@@ -786,7 +790,7 @@ func (m *ResourceTrustsModule) getSecretsManagerSecretsPoliciesPerRegion(r strin
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxSecretsManagerClient := InitSecretsManagerClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
cloudFoxSecretsManagerClient := InitSecretsManagerClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListSecrets, err := sdk.CachedSecretsManagerListSecrets(cloudFoxSecretsManagerClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
@@ -852,7 +856,7 @@ func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sy
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxGlueClient := InitGlueClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
cloudFoxGlueClient := InitGlueClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ResourcePolicies, err := sdk.CachedGlueGetResourcePolicies(cloudFoxGlueClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
|
||||
+152
-131
@@ -1,22 +1,19 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/bishopfox/knownawsaccountslookup"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -42,6 +39,8 @@ type RoleTrustsModule struct {
|
||||
AnalyzedRoles []AnalyzedRole
|
||||
RoleTrustTable []RoleTrustRow
|
||||
|
||||
vendors *knownawsaccountslookup.Vendors
|
||||
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
|
||||
@@ -62,46 +61,12 @@ type RoleTrustRow struct {
|
||||
|
||||
type AnalyzedRole struct {
|
||||
roleARN *string
|
||||
trustsDoc trustPolicyDocument
|
||||
trustsDoc policy.TrustPolicyDocument
|
||||
// trustType string // UNUSED FIELD, PLEASE REVIEW
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
type trustPolicyDocument struct {
|
||||
Version string `json:"Version"`
|
||||
Statement []RoleTrustStatementEntry `json:"Statement"`
|
||||
}
|
||||
|
||||
type RoleTrustStatementEntry struct {
|
||||
Sid string `json:"Sid"`
|
||||
Effect string `json:"Effect"`
|
||||
Principal struct {
|
||||
AWS ListOfPrincipals `json:"AWS"`
|
||||
Service ListOfPrincipals `json:"Service"`
|
||||
Federated ListOfPrincipals `json:"Federated"`
|
||||
} `json:"Principal"`
|
||||
Action string `json:"Action"`
|
||||
Condition struct {
|
||||
StringEquals struct {
|
||||
StsExternalID string `json:"sts:ExternalId"`
|
||||
SAMLAud string `json:"SAML:aud"`
|
||||
OidcEksSub string `json:"OidcEksSub"`
|
||||
OidcEksAud string `json:"OidcEksAud"`
|
||||
CognitoAud string `json:"cognito-identity.amazonaws.com:aud"`
|
||||
} `json:"StringEquals"`
|
||||
StringLike struct {
|
||||
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
|
||||
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
|
||||
OidcEksSub string `json:"OidcEksSub"`
|
||||
OidcEksAud string `json:"OidcEksAud"`
|
||||
} `json:"StringLike"`
|
||||
ForAnyValueStringLike struct {
|
||||
CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"`
|
||||
} `json:"ForAnyValue:StringLike"`
|
||||
} `json:"Condition"`
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int) {
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
@@ -113,6 +78,9 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
localAdminMap := make(map[string]bool)
|
||||
m.vendors = knownawsaccountslookup.NewVendorMap()
|
||||
m.vendors.PopulateKnownAWSAccounts()
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
//fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
@@ -134,6 +102,7 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
@@ -183,12 +152,12 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
|
||||
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)))
|
||||
fmt.Printf("[%s][%s] %s service 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)))
|
||||
fmt.Printf("[%s][%s] %s federated 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))
|
||||
}
|
||||
@@ -235,6 +204,14 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, principal := range statement.Principal.AWS {
|
||||
//check to see if the accountID is known
|
||||
if strings.Contains(principal, "arn:aws:iam::") || strings.Contains(principal, "root") {
|
||||
accountID := strings.Split(principal, ":")[4]
|
||||
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
|
||||
if vendorName != "" {
|
||||
principal = fmt.Sprintf("%s (%s)", principal, vendorName)
|
||||
}
|
||||
}
|
||||
|
||||
RoleTrustRow := RoleTrustRow{
|
||||
RoleARN: aws.ToString(role.roleARN),
|
||||
@@ -254,6 +231,7 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin
|
||||
RoleTrustRow.CanPrivEsc})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
@@ -302,6 +280,11 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string)
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, principal := range statement.Principal.AWS {
|
||||
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == "" {
|
||||
accountID := strings.Split(principal, ":")[4]
|
||||
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
|
||||
if vendorName != "" {
|
||||
principal = fmt.Sprintf("%s (%s)", principal, vendorName)
|
||||
}
|
||||
|
||||
RoleTrustRow := RoleTrustRow{
|
||||
RoleARN: aws.ToString(role.roleARN),
|
||||
@@ -353,7 +336,7 @@ func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string,
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{"Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
tableCols = []string{"Account", "Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
// Otherwise, use the default columns for this module (brief)
|
||||
} else {
|
||||
tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
@@ -421,7 +404,7 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{"Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
tableCols = []string{"Account", "Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
// Otherwise, use the default columns for this module (brief)
|
||||
} else {
|
||||
tableCols = []string{"Role Name", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
@@ -434,23 +417,25 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
if len(statement.Principal.Federated) > 0 {
|
||||
provider, subject := parseFederatedTrustPolicy(statement)
|
||||
RoleTrustRow := RoleTrustRow{
|
||||
RoleARN: aws.ToString(role.roleARN),
|
||||
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
|
||||
TrustedFederatedProvider: provider,
|
||||
TrustedFederatedSubject: subject,
|
||||
IsAdmin: role.Admin,
|
||||
CanPrivEsc: role.CanPrivEsc,
|
||||
provider, subjects := parseFederatedTrustPolicy(statement)
|
||||
for _, subject := range subjects {
|
||||
RoleTrustRow := RoleTrustRow{
|
||||
RoleARN: aws.ToString(role.roleARN),
|
||||
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
|
||||
TrustedFederatedProvider: provider,
|
||||
TrustedFederatedSubject: subject,
|
||||
IsAdmin: role.Admin,
|
||||
CanPrivEsc: role.CanPrivEsc,
|
||||
}
|
||||
body = append(body, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
RoleTrustRow.RoleARN,
|
||||
RoleTrustRow.RoleName,
|
||||
RoleTrustRow.TrustedFederatedProvider,
|
||||
RoleTrustRow.TrustedFederatedSubject,
|
||||
RoleTrustRow.IsAdmin,
|
||||
RoleTrustRow.CanPrivEsc})
|
||||
}
|
||||
body = append(body, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
RoleTrustRow.RoleARN,
|
||||
RoleTrustRow.RoleName,
|
||||
RoleTrustRow.TrustedFederatedProvider,
|
||||
RoleTrustRow.TrustedFederatedSubject,
|
||||
RoleTrustRow.IsAdmin,
|
||||
RoleTrustRow.CanPrivEsc})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -461,47 +446,120 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
|
||||
|
||||
}
|
||||
|
||||
func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) {
|
||||
var column2, column3 string
|
||||
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
|
||||
column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")"
|
||||
trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n")
|
||||
if trustedRepos == "" {
|
||||
column3 = "ALL REPOS!!!"
|
||||
func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string, []string) {
|
||||
var provider string
|
||||
var subjects []string
|
||||
if len(statement.Principal.Federated) > 1 {
|
||||
sharedLogger.Warnf("Multiple federated providers found in the trust policy. This is not currently supported. Please review the trust policy for specifics.")
|
||||
provider = "Multiple Federated Providers"
|
||||
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
|
||||
}
|
||||
|
||||
switch {
|
||||
// lets use the Federated field to determine the provider, then based on the provider we can grab the list of subjects
|
||||
case strings.Contains(statement.Principal.Federated[0], "token.actions.githubusercontent.com"):
|
||||
provider = "GitHub"
|
||||
if len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.TokenActionsGithubusercontentComSub...)
|
||||
} else if len(statement.Condition.StringEquals.TokenActionsGithubusercontentComSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.TokenActionsGithubusercontentComSub...)
|
||||
} else {
|
||||
column3 = trustedRepos
|
||||
subjects = append(subjects, "ALL REPOS!!!")
|
||||
}
|
||||
} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
|
||||
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
|
||||
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
|
||||
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
|
||||
column2 = "Okta" // (" + statement.Principal.Federated[0] + ")"
|
||||
}
|
||||
column3 = "Not applicable"
|
||||
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" {
|
||||
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
|
||||
if statement.Condition.StringEquals.OidcEksSub != "" {
|
||||
column3 = statement.Condition.StringEquals.OidcEksSub
|
||||
} else if statement.Condition.StringLike.OidcEksSub != "" {
|
||||
column3 = statement.Condition.StringLike.OidcEksSub
|
||||
case strings.Contains(statement.Principal.Federated[0], "oidc.eks"):
|
||||
provider = "EKS"
|
||||
if len(statement.Condition.StringLike.OidcEksSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.OidcEksSub...)
|
||||
} else if len(statement.Condition.StringEquals.OidcEksSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.OidcEksSub...)
|
||||
} else {
|
||||
column3 = "ALL SERVICE ACCOUNTS!"
|
||||
subjects = append(subjects, "ALL SERVICE ACCOUNTS!!!")
|
||||
}
|
||||
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
|
||||
column2 = "Cognito" // (" + statement.Principal.Federated[0] + ")"
|
||||
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
|
||||
column3 = statement.Condition.ForAnyValueStringLike.CognitoAMR
|
||||
// terraform case
|
||||
case strings.Contains(statement.Principal.Federated[0], "app.terraform.io"):
|
||||
provider = "Terraform Cloud"
|
||||
if len(statement.Condition.StringLike.TerraformSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.TerraformSub...)
|
||||
} else if len(statement.Condition.StringEquals.TerraformSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.TerraformSub...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL WORKSPACES")
|
||||
}
|
||||
} else {
|
||||
if column2 == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") {
|
||||
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
|
||||
column3 = "ALL SERVICE ACCOUNTS!"
|
||||
} else if column2 == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
|
||||
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
|
||||
// Azure AD case
|
||||
case strings.Contains(statement.Principal.Federated[0], "http://sts.windows.net"):
|
||||
provider = "Azure AD"
|
||||
if len(statement.Condition.StringLike.AzureADIss) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.AzureADIss...)
|
||||
} else if len(statement.Condition.StringEquals.AzureADIss) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.AzureADIss...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL ISSUERS")
|
||||
}
|
||||
|
||||
// AWS SSO case
|
||||
case strings.Contains(statement.Principal.Federated[0], "AWSSSO"):
|
||||
provider = "AWS SSO"
|
||||
subjects = append(subjects, "Not applicable")
|
||||
|
||||
// okta case
|
||||
case strings.Contains(statement.Principal.Federated[0], "Okta"):
|
||||
provider = "Okta"
|
||||
subjects = append(subjects, "Not applicable")
|
||||
|
||||
// cognito case
|
||||
case statement.Principal.Federated[0] == "cognito-identity.amazonaws.com":
|
||||
provider = "Cognito"
|
||||
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
|
||||
subjects = append(subjects, statement.Condition.ForAnyValueStringLike.CognitoAMR)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL IDENTITIES")
|
||||
}
|
||||
// google workspace case
|
||||
case strings.Contains(statement.Principal.Federated[0], "workspace.google.com"):
|
||||
provider = "Google Workspace"
|
||||
if len(statement.Condition.StringLike.GoogleWorkspaceSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.GoogleWorkspaceSub...)
|
||||
} else if len(statement.Condition.StringEquals.GoogleWorkspaceSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.GoogleWorkspaceSub...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL USERS")
|
||||
}
|
||||
// GCP case
|
||||
case strings.Contains(statement.Principal.Federated[0], "accounts.google.com"):
|
||||
provider = "GCP"
|
||||
if len(statement.Condition.StringLike.GCPSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.GCPSub...)
|
||||
} else if len(statement.Condition.StringEquals.GCPSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.GCPSub...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL USERS")
|
||||
}
|
||||
// auth0 case
|
||||
//not ready yet
|
||||
// case strings.Contains(statement.Principal.Federated[0], "auth0.com"):
|
||||
// provider = "Auth0"
|
||||
// if len(statement.Condition.ForAnyValueStringLike.Auth0Amr) > 0 {
|
||||
// subjects = append(subjects, statement.Condition.ForAnyValueStringLike.Auth0Amr...)
|
||||
// } else {
|
||||
// subjects = append(subjects, "ALL GROUPS")
|
||||
// }
|
||||
// circleci case
|
||||
case strings.Contains(statement.Principal.Federated[0], "oidc.circleci.com"):
|
||||
provider = "CircleCI"
|
||||
if len(statement.Condition.StringLike.CircleCIAud) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.CircleCIAud...)
|
||||
} else if len(statement.Condition.StringEquals.CircleCIAud) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.CircleCIAud...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL PROJECTS")
|
||||
}
|
||||
|
||||
default:
|
||||
provider = "Unknown Federated Provider"
|
||||
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
|
||||
|
||||
}
|
||||
return column2, column3
|
||||
return provider, subjects
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
|
||||
@@ -521,7 +579,7 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
|
||||
}
|
||||
|
||||
for _, role := range ListRoles {
|
||||
trustsdoc, err := parseRoleTrustPolicyDocument(role)
|
||||
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
@@ -541,40 +599,3 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func parseRoleTrustPolicyDocument(role types.Role) (trustPolicyDocument, error) {
|
||||
document, _ := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument))
|
||||
|
||||
// These next six lines are a hack, needed because the EKS OIDC json field name is dynamic
|
||||
// and therefore can't be used to unmarshall in a predictable way. The hack involves replacing
|
||||
// the random pattern with a predictable one so that we can add the predictable one in the struct
|
||||
// used to unmarshall.
|
||||
pattern := `(\w+)\:`
|
||||
pattern2 := `".[a-zA-Z0-9\-\.]+/id/`
|
||||
var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub")
|
||||
var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud")
|
||||
document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub")
|
||||
document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud")
|
||||
|
||||
var parsedDocumentToJSON trustPolicyDocument
|
||||
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
|
||||
return parsedDocumentToJSON, nil
|
||||
}
|
||||
|
||||
// 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 ListOfPrincipals []string
|
||||
|
||||
func (r *ListOfPrincipals) 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")
|
||||
}
|
||||
+96
-14
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigateway"
|
||||
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type APIGatewayClientInterface interface {
|
||||
@@ -22,13 +23,32 @@ type APIGatewayClientInterface interface {
|
||||
GetUsagePlanKeys(context.Context, *apigateway.GetUsagePlanKeysInput, ...func(*apigateway.Options)) (*apigateway.GetUsagePlanKeysOutput, error)
|
||||
}
|
||||
|
||||
type CachedGetMethodOutput struct {
|
||||
ApiKeyRequired *bool
|
||||
AuthorizationScopes []string
|
||||
AuthorizationType *string
|
||||
AuthorizerId *string
|
||||
HttpMethod *string
|
||||
MethodIntegration *apiGatewayTypes.Integration
|
||||
MethodResponses map[string]apiGatewayTypes.MethodResponse
|
||||
OperationName *string
|
||||
RequestModels map[string]string
|
||||
RequestParameters map[string]bool
|
||||
RequestValidatorId *string
|
||||
ResourceId *string
|
||||
}
|
||||
|
||||
type CachedGetStagesOutput struct {
|
||||
Item []apiGatewayTypes.Stage
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]apiGatewayTypes.RestApi{})
|
||||
gob.Register(apigateway.GetStagesOutput{})
|
||||
gob.Register(CachedGetStagesOutput{})
|
||||
gob.Register([]apiGatewayTypes.Resource{})
|
||||
gob.Register([]apiGatewayTypes.DomainName{})
|
||||
gob.Register([]apiGatewayTypes.BasePathMapping{})
|
||||
gob.Register(apigateway.GetMethodOutput{})
|
||||
gob.Register(CachedGetMethodOutput{})
|
||||
gob.Register([]apiGatewayTypes.UsagePlan{})
|
||||
gob.Register([]apiGatewayTypes.UsagePlanKey{})
|
||||
}
|
||||
@@ -66,16 +86,18 @@ func CachedApiGatewayGetRestAPIs(client APIGatewayClientInterface, accountID str
|
||||
}
|
||||
PaginationControl = GetRestApis.Position
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, restAPIs, cache.DefaultExpiration)
|
||||
return restAPIs, nil
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetStages function that accepts a client, account id, region, and rest api id. Make sure it handles caching, the region option and pagination
|
||||
func CachedApiGatewayGetStages(client APIGatewayClientInterface, accountID string, region string, restAPIID string) (apigateway.GetStagesOutput, error) {
|
||||
func CachedApiGatewayGetStages(client APIGatewayClientInterface, accountID string, region string, restAPIID string) (*apigateway.GetStagesOutput, error) {
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetStages-%s-%s", accountID, region, restAPIID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(apigateway.GetStagesOutput), nil
|
||||
// Convert cached data back to GetStagesOutput before returning
|
||||
cachedOutput := cached.(*CachedGetStagesOutput) // Ensure this type assertion matches your caching logic
|
||||
return fromCachedGetStagesOutput(cachedOutput), nil
|
||||
}
|
||||
|
||||
GetStages, err := client.GetStages(
|
||||
@@ -89,10 +111,28 @@ func CachedApiGatewayGetStages(client APIGatewayClientInterface, accountID strin
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return apigateway.GetStagesOutput{}, err
|
||||
return &apigateway.GetStagesOutput{}, err
|
||||
}
|
||||
|
||||
return *GetStages, err
|
||||
// Convert GetStagesOutput to CachedGetStagesOutput before caching
|
||||
cachedVersion := toCachedGetStagesOutput(GetStages)
|
||||
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
|
||||
return GetStages, err
|
||||
}
|
||||
|
||||
// Convert from AWS SDK type to custom type for caching
|
||||
func toCachedGetStagesOutput(gso *apigateway.GetStagesOutput) *CachedGetStagesOutput {
|
||||
return &CachedGetStagesOutput{
|
||||
Item: gso.Item,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to AWS SDK type after fetching from cache
|
||||
func fromCachedGetStagesOutput(cgso *CachedGetStagesOutput) *apigateway.GetStagesOutput {
|
||||
return &apigateway.GetStagesOutput{
|
||||
Item: cgso.Item,
|
||||
// Initialize ResultMetadata or leave it as zero value if it's not required for your use case
|
||||
}
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetResources function that accepts a client, account id, region, and rest api id. Make sure it handles caching, the region option and pagination
|
||||
@@ -129,7 +169,7 @@ func CachedApiGatewayGetResources(client APIGatewayClientInterface, accountID st
|
||||
}
|
||||
PaginationControl = GetResources.Position
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, resources, cache.DefaultExpiration)
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
@@ -166,7 +206,7 @@ func CachedApiGatewayGetDomainNames(client APIGatewayClientInterface, accountID
|
||||
}
|
||||
PaginationControl = GetDomainNames.Position
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, domainNames, cache.DefaultExpiration)
|
||||
return domainNames, nil
|
||||
}
|
||||
|
||||
@@ -204,17 +244,19 @@ func CachedApiGatewayGetBasePathMappings(client APIGatewayClientInterface, accou
|
||||
}
|
||||
PaginationControl = GetBasePathMappings.Position
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, basePathMappings, cache.DefaultExpiration)
|
||||
return basePathMappings, nil
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetMethod function that accepts a client, account id, region, rest api id, and resource id. Make sure it handles caching, the region option and pagination if needed
|
||||
func CachedApiGatewayGetMethod(client APIGatewayClientInterface, accountID string, region string, restAPIID string, resourceID string, method string) (apigateway.GetMethodOutput, error) {
|
||||
func CachedApiGatewayGetMethod(client APIGatewayClientInterface, accountID string, region string, restAPIID string, resourceID string, method string) (*apigateway.GetMethodOutput, error) {
|
||||
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetMethod-%s-%s-%s-%s", accountID, region, restAPIID, resourceID, method)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(apigateway.GetMethodOutput), nil
|
||||
// Convert cached data back to GetMethodOutput before returning
|
||||
cachedOutput := cached.(*CachedGetMethodOutput) // Ensure this type assertion matches your caching logic
|
||||
return fromCachedGetMethodOutput(cachedOutput), nil
|
||||
}
|
||||
|
||||
GetMethod, err := client.GetMethod(
|
||||
@@ -230,13 +272,51 @@ func CachedApiGatewayGetMethod(client APIGatewayClientInterface, accountID strin
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return apigateway.GetMethodOutput{}, err
|
||||
return &apigateway.GetMethodOutput{}, err
|
||||
}
|
||||
|
||||
return *GetMethod, nil
|
||||
// Convert GetMethodOutput to CachedGetMethodOutput before caching
|
||||
cachedVersion := toCachedGetMethodOutput(GetMethod)
|
||||
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
|
||||
return GetMethod, nil
|
||||
|
||||
}
|
||||
|
||||
// Convert from AWS SDK type to custom type for caching
|
||||
func toCachedGetMethodOutput(gmo *apigateway.GetMethodOutput) *CachedGetMethodOutput {
|
||||
return &CachedGetMethodOutput{
|
||||
ApiKeyRequired: gmo.ApiKeyRequired,
|
||||
AuthorizationScopes: gmo.AuthorizationScopes,
|
||||
AuthorizationType: gmo.AuthorizationType,
|
||||
AuthorizerId: gmo.AuthorizerId,
|
||||
HttpMethod: gmo.HttpMethod,
|
||||
MethodIntegration: gmo.MethodIntegration,
|
||||
MethodResponses: gmo.MethodResponses,
|
||||
OperationName: gmo.OperationName,
|
||||
RequestModels: gmo.RequestModels,
|
||||
RequestParameters: gmo.RequestParameters,
|
||||
RequestValidatorId: gmo.RequestValidatorId,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to AWS SDK type after fetching from cache
|
||||
func fromCachedGetMethodOutput(cgmo *CachedGetMethodOutput) *apigateway.GetMethodOutput {
|
||||
return &apigateway.GetMethodOutput{
|
||||
ApiKeyRequired: cgmo.ApiKeyRequired,
|
||||
AuthorizationScopes: cgmo.AuthorizationScopes,
|
||||
AuthorizationType: cgmo.AuthorizationType,
|
||||
AuthorizerId: cgmo.AuthorizerId,
|
||||
HttpMethod: cgmo.HttpMethod,
|
||||
MethodIntegration: cgmo.MethodIntegration,
|
||||
MethodResponses: cgmo.MethodResponses,
|
||||
OperationName: cgmo.OperationName,
|
||||
RequestModels: cgmo.RequestModels,
|
||||
RequestParameters: cgmo.RequestParameters,
|
||||
RequestValidatorId: cgmo.RequestValidatorId,
|
||||
// ResultMetadata: This will be missing or zero value; handle accordingly
|
||||
}
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetUsagePlans function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
|
||||
func CachedApiGatewayGetUsagePlans(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.UsagePlan, error) {
|
||||
var PaginationControl *string
|
||||
@@ -271,6 +351,7 @@ func CachedApiGatewayGetUsagePlans(client APIGatewayClientInterface, accountID s
|
||||
PaginationControl = GetUsagePlans.Position
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, usagePlans, cache.DefaultExpiration)
|
||||
return usagePlans, nil
|
||||
}
|
||||
|
||||
@@ -309,5 +390,6 @@ func CachedApiGatewayGetUsagePlanKeys(client APIGatewayClientInterface, accountI
|
||||
PaginationControl = GetUsagePlanKeys.Position
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, usagePlanKeys, cache.DefaultExpiration)
|
||||
return usagePlanKeys, nil
|
||||
}
|
||||
+117
-2
@@ -15,12 +15,127 @@ func (m *MockedAWSAPIGatewayClient) GetRestApis(ctx context.Context, input *apig
|
||||
return &apigateway.GetRestApisOutput{
|
||||
Items: []apiGatewayTypes.RestApi{
|
||||
{
|
||||
Id: aws.String("api1"),
|
||||
Id: aws.String("abcdefg"),
|
||||
Name: aws.String("api1"),
|
||||
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
|
||||
Types: []apiGatewayTypes.EndpointType{
|
||||
apiGatewayTypes.EndpointTypePrivate,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: aws.String("api2"),
|
||||
Id: aws.String("qwerty"),
|
||||
Name: aws.String("api2"),
|
||||
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
|
||||
Types: []apiGatewayTypes.EndpointType{
|
||||
apiGatewayTypes.EndpointTypeRegional,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetStages(ctx context.Context, input *apigateway.GetStagesInput, options ...func(*apigateway.Options)) (*apigateway.GetStagesOutput, error) {
|
||||
return &apigateway.GetStagesOutput{
|
||||
Item: []apiGatewayTypes.Stage{
|
||||
{
|
||||
StageName: aws.String("stage1"),
|
||||
},
|
||||
{
|
||||
StageName: aws.String("stage2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetResources(ctx context.Context, input *apigateway.GetResourcesInput, options ...func(*apigateway.Options)) (*apigateway.GetResourcesOutput, error) {
|
||||
return &apigateway.GetResourcesOutput{
|
||||
Items: []apiGatewayTypes.Resource{
|
||||
{
|
||||
Id: aws.String("resource1"),
|
||||
Path: aws.String("/path1"),
|
||||
ResourceMethods: map[string]apiGatewayTypes.Method{
|
||||
"GET": {
|
||||
ApiKeyRequired: aws.Bool(true),
|
||||
AuthorizerId: aws.String("authorizer1"),
|
||||
OperationName: aws.String("operation1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: aws.String("resource2"),
|
||||
Path: aws.String("/path2"),
|
||||
ResourceMethods: map[string]apiGatewayTypes.Method{
|
||||
"ANY": {
|
||||
ApiKeyRequired: aws.Bool(true),
|
||||
AuthorizerId: aws.String("authorizer2"),
|
||||
OperationName: aws.String("operation2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetDomainNames(ctx context.Context, input *apigateway.GetDomainNamesInput, options ...func(*apigateway.Options)) (*apigateway.GetDomainNamesOutput, error) {
|
||||
return &apigateway.GetDomainNamesOutput{
|
||||
Items: []apiGatewayTypes.DomainName{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetBasePathMappings(ctx context.Context, input *apigateway.GetBasePathMappingsInput, options ...func(*apigateway.Options)) (*apigateway.GetBasePathMappingsOutput, error) {
|
||||
return &apigateway.GetBasePathMappingsOutput{
|
||||
Items: []apiGatewayTypes.BasePathMapping{
|
||||
{
|
||||
BasePath: aws.String("basepath1"),
|
||||
RestApiId: aws.String("abcdefg"),
|
||||
Stage: aws.String("stage1"),
|
||||
},
|
||||
{
|
||||
BasePath: aws.String("basepath2"),
|
||||
RestApiId: aws.String("qwerty"),
|
||||
Stage: aws.String("stage2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetMethod(ctx context.Context, input *apigateway.GetMethodInput, options ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error) {
|
||||
return &apigateway.GetMethodOutput{
|
||||
ApiKeyRequired: aws.Bool(true),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetUsagePlans(ctx context.Context, input *apigateway.GetUsagePlansInput, options ...func(*apigateway.Options)) (*apigateway.GetUsagePlansOutput, error) {
|
||||
return &apigateway.GetUsagePlansOutput{
|
||||
Items: []apiGatewayTypes.UsagePlan{
|
||||
{
|
||||
Id: aws.String("usageplan1"),
|
||||
Name: aws.String("usageplan1"),
|
||||
ApiStages: []apiGatewayTypes.ApiStage{
|
||||
{
|
||||
ApiId: aws.String("abcdefg"),
|
||||
Stage: aws.String("stage2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetUsagePlanKeys(ctx context.Context, input *apigateway.GetUsagePlanKeysInput, options ...func(*apigateway.Options)) (*apigateway.GetUsagePlanKeysOutput, error) {
|
||||
return &apigateway.GetUsagePlanKeysOutput{
|
||||
Items: []apiGatewayTypes.UsagePlanKey{
|
||||
{
|
||||
Id: aws.String("usageplankey1"),
|
||||
Type: aws.String("API_KEY"),
|
||||
Value: aws.String("23oieuwefo3rfs"),
|
||||
},
|
||||
{
|
||||
Id: aws.String("usageplankey2"),
|
||||
Type: aws.String("API_KEY"),
|
||||
Value: aws.String("982yf98fdv8dlds"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
@@ -21,13 +21,14 @@ type APIGatewayv2ClientInterface interface {
|
||||
}
|
||||
|
||||
type domainName apiGatwayV2Types.DomainName
|
||||
type stage apiGatwayV2Types.Stage
|
||||
|
||||
func init() {
|
||||
gob.Register([]apiGatwayV2Types.Api{})
|
||||
//need to do this to avoid conflicts with the Instance type in the ec2 package
|
||||
gob.Register([]domainName{})
|
||||
gob.Register([]apiGatwayV2Types.ApiMapping{})
|
||||
gob.Register([]apiGatwayV2Types.Stage{})
|
||||
gob.Register([]stage{})
|
||||
gob.Register([]apiGatwayV2Types.Route{})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,68 @@ func (m *MockedAWSAPIGatewayv2Client) GetApis(ctx context.Context, input *apigat
|
||||
return &apigatewayv2.GetApisOutput{
|
||||
Items: []apiGatwayV2Types.Api{
|
||||
{
|
||||
ApiId: aws.String("api1"),
|
||||
Name: aws.String("api1"),
|
||||
ApiId: aws.String("asdfsdfasdf"),
|
||||
Name: aws.String("api1"),
|
||||
ApiEndpoint: aws.String("https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com"),
|
||||
},
|
||||
{
|
||||
ApiId: aws.String("api2"),
|
||||
Name: aws.String("api2"),
|
||||
ApiId: aws.String("qwertyqwerty"),
|
||||
Name: aws.String("api2"),
|
||||
ApiEndpoint: aws.String("https://qwertyqwerty.execute-api.us-east-1.amazonaws.com"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetDomainNames(ctx context.Context, input *apigatewayv2.GetDomainNamesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetDomainNamesOutput, error) {
|
||||
return &apigatewayv2.GetDomainNamesOutput{
|
||||
Items: []apiGatwayV2Types.DomainName{
|
||||
{
|
||||
DomainName: aws.String("domain1"),
|
||||
},
|
||||
{
|
||||
DomainName: aws.String("domain2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetApiMappings(ctx context.Context, input *apigatewayv2.GetApiMappingsInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApiMappingsOutput, error) {
|
||||
return &apigatewayv2.GetApiMappingsOutput{
|
||||
Items: []apiGatwayV2Types.ApiMapping{
|
||||
{
|
||||
ApiMappingId: aws.String("apiMapping1"),
|
||||
},
|
||||
{
|
||||
ApiMappingId: aws.String("apiMapping2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetStages(ctx context.Context, input *apigatewayv2.GetStagesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetStagesOutput, error) {
|
||||
return &apigatewayv2.GetStagesOutput{
|
||||
Items: []apiGatwayV2Types.Stage{
|
||||
{
|
||||
StageName: aws.String("stage1"),
|
||||
},
|
||||
{
|
||||
StageName: aws.String("stage2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetRoutes(ctx context.Context, input *apigatewayv2.GetRoutesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetRoutesOutput, error) {
|
||||
return &apigatewayv2.GetRoutesOutput{
|
||||
Items: []apiGatwayV2Types.Route{
|
||||
{
|
||||
RouteId: aws.String("route1"),
|
||||
RouteKey: aws.String("POST /route1"),
|
||||
},
|
||||
{
|
||||
RouteId: aws.String("route2"),
|
||||
RouteKey: aws.String("GET /route2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
@@ -87,7 +87,7 @@ func CachedCodeBuildBatchGetProjects(CodeBuildClient CodeBuildClientInterface, a
|
||||
return project, err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, BatchGetProjects.Projects[0], cache.DefaultExpiration)
|
||||
internal.Cache.Set(cacheKey, BatchGetProjects.Projects, cache.DefaultExpiration)
|
||||
return BatchGetProjects.Projects[0], nil
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ type DocDBClientInterface interface {
|
||||
|
||||
func init() {
|
||||
gob.Register([]docdbTypes.GlobalCluster{})
|
||||
gob.Register([]docdbTypes.DBCluster{})
|
||||
//gob.Register([]docdbTypes.DBCluster{})
|
||||
//gob.Register([]docdbTypes.DBInstance{})
|
||||
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ import (
|
||||
type MockedAWSDynamoDBClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSDynamoDBClient) ListTables() ([]string, error) {
|
||||
return []string{"table1", "table2"}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSDynamoDBClient) DescribeTable(ctx context.Context, input *dynamodb.DescribeTableInput, options ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
|
||||
return &dynamodb.DescribeTableOutput{
|
||||
Table: &dynamodbTypes.TableDescription{
|
||||
@@ -21,3 +17,9 @@ func (m *MockedAWSDynamoDBClient) DescribeTable(ctx context.Context, input *dyna
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSDynamoDBClient) ListTables(ctx context.Context, input *dynamodb.ListTablesInput, options ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) {
|
||||
return &dynamodb.ListTablesOutput{
|
||||
TableNames: []string{"table1", "table2"},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/neptune"
|
||||
neptuneTypes "github.com/aws/aws-sdk-go-v2/service/neptune/types"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type NeptuneClientInterface interface {
|
||||
DescribeDBClusters(ctx context.Context, params *neptune.DescribeDBClustersInput, optFns ...func(*neptune.Options)) (*neptune.DescribeDBClustersOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
//gob.RegisterName("neptune.DBCluster", []neptuneTypes.DBCluster{})
|
||||
}
|
||||
|
||||
func CachedNeptuneDescribeDBClusters(client NeptuneClientInterface, accountID string, region string) ([]neptuneTypes.DBCluster, error) {
|
||||
var paginationControl *string
|
||||
var clusters []neptuneTypes.DBCluster
|
||||
cacheKey := fmt.Sprintf("%s-neptune-DescribeDBClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]neptuneTypes.DBCluster), nil
|
||||
}
|
||||
for {
|
||||
describeDBClusters, err := client.DescribeDBClusters(
|
||||
context.TODO(),
|
||||
&neptune.DescribeDBClustersInput{
|
||||
Marker: paginationControl,
|
||||
},
|
||||
func(o *neptune.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, describeDBClusters.DBClusters...)
|
||||
|
||||
//pagination
|
||||
if describeDBClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
paginationControl = describeDBClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
|
||||
return clusters, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/neptune"
|
||||
neptuneTypes "github.com/aws/aws-sdk-go-v2/service/neptune/types"
|
||||
)
|
||||
|
||||
type MockedNeptuneClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedNeptuneClient) DescribeDBClusters(ctx context.Context, params *neptune.DescribeDBClustersInput, optFns ...func(*neptune.Options)) (*neptune.DescribeDBClustersOutput, error) {
|
||||
return &neptune.DescribeDBClustersOutput {
|
||||
DBClusters: []neptuneTypes.DBCluster{
|
||||
{
|
||||
DBClusterIdentifier: aws.String("db1"),
|
||||
Engine: aws.String("neptune"),
|
||||
EngineVersion: aws.String("1.3.0.0"),
|
||||
ClusterCreateTime: aws.Time(time.Now()),
|
||||
MasterUsername: aws.String("neptune"),
|
||||
},
|
||||
{
|
||||
DBClusterIdentifier: aws.String("db2"),
|
||||
Engine: aws.String("neptune"),
|
||||
EngineVersion: aws.String("1.3.0.0"),
|
||||
ClusterCreateTime: aws.Time(time.Now()),
|
||||
MasterUsername: aws.String("neptune"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -14,10 +14,13 @@ import (
|
||||
|
||||
type RDSClientInterface interface {
|
||||
DescribeDBInstances(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error)
|
||||
DescribeDBClusters(context.Context, *rds.DescribeDBClustersInput, ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]rdsTypes.DBInstance{})
|
||||
gob.Register([]rdsTypes.DBCluster{})
|
||||
|
||||
}
|
||||
|
||||
func CachedRDSDescribeDBInstances(client RDSClientInterface, accountID string, region string) ([]rdsTypes.DBInstance, error) {
|
||||
@@ -55,3 +58,39 @@ func CachedRDSDescribeDBInstances(client RDSClientInterface, accountID string, r
|
||||
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func CachedRDSDescribeDBClusters(client RDSClientInterface, accountID string, region string) ([]rdsTypes.DBCluster, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []rdsTypes.DBCluster
|
||||
cacheKey := fmt.Sprintf("%s-rds-DescribeDBClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]rdsTypes.DBCluster), nil
|
||||
}
|
||||
for {
|
||||
DescribeDBClusters, err := client.DescribeDBClusters(
|
||||
context.TODO(),
|
||||
&rds.DescribeDBClustersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *rds.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, DescribeDBClusters.DBClusters...)
|
||||
|
||||
//pagination
|
||||
if DescribeDBClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeDBClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
|
||||
return clusters, nil
|
||||
}
|
||||
+52
-2
@@ -9,10 +9,10 @@ import (
|
||||
rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
|
||||
)
|
||||
|
||||
type MOckedRDSClient struct {
|
||||
type MockedRDSClient struct {
|
||||
}
|
||||
|
||||
func (m *MOckedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.DescribeDBInstancesInput, options ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) {
|
||||
func (m *MockedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.DescribeDBInstancesInput, options ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) {
|
||||
return &rds.DescribeDBInstancesOutput{
|
||||
DBInstances: []rdsTypes.DBInstance{
|
||||
{
|
||||
@@ -32,3 +32,53 @@ func (m *MOckedRDSClient) DescribeDBInstances(ctx context.Context, input *rds.De
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedRDSClient) DescribeDBClusters(ctx context.Context, input *rds.DescribeDBClustersInput, options ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error) {
|
||||
return &rds.DescribeDBClustersOutput{
|
||||
DBClusters: []rdsTypes.DBCluster{
|
||||
{
|
||||
DBClusterIdentifier: aws.String("db1"),
|
||||
Engine: aws.String("aurora-postgresql"),
|
||||
EngineVersion: aws.String("13.3"),
|
||||
Endpoint: aws.String("db1.cluster-123456789012.us-west-2.rds.amazonaws.com"),
|
||||
ClusterCreateTime: aws.Time(time.Now()),
|
||||
MasterUsername: aws.String("postgres"),
|
||||
Port: aws.Int32(5432),
|
||||
AssociatedRoles: nil,
|
||||
},
|
||||
{
|
||||
DBClusterIdentifier: aws.String("db2"),
|
||||
Engine: aws.String("aurora-postgresql"),
|
||||
EngineVersion: aws.String("13.3"),
|
||||
Endpoint: aws.String("db2.cluster-123456789012.us-west-2.rds.amazonaws.com"),
|
||||
ClusterCreateTime: aws.Time(time.Now()),
|
||||
MasterUsername: aws.String("postgres"),
|
||||
Port: aws.Int32(5432),
|
||||
AssociatedRoles: nil,
|
||||
},
|
||||
{
|
||||
DBClusterIdentifier: aws.String("db3"),
|
||||
Engine: aws.String("neptune"),
|
||||
EngineVersion: aws.String("1.2.1.0"),
|
||||
Endpoint: aws.String("db3.cluster-123456789012.us-west-2.neptune.amazonaws.com"),
|
||||
MasterUsername: aws.String("neptune"),
|
||||
Port: aws.Int32(8182),
|
||||
AssociatedRoles: []rdsTypes.DBClusterRole{
|
||||
{
|
||||
RoleArn: aws.String("arn:aws:iam::123456789012:role/NeptuneRole"),
|
||||
Status: aws.String("active"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
DBClusterIdentifier: aws.String("db4"),
|
||||
Engine: aws.String("docsdb"),
|
||||
EngineVersion: aws.String("4.0.0"),
|
||||
Endpoint: aws.String("db4.cluster-123456789012.us-west-2.docdb.amazonaws.com"),
|
||||
MasterUsername: aws.String("docsdb"),
|
||||
Port: aws.Int32(27017),
|
||||
AssociatedRoles: nil,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
+55
-33
@@ -42,6 +42,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/kinesis"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lightsail"
|
||||
"github.com/aws/aws-sdk-go-v2/service/neptune"
|
||||
"github.com/aws/aws-sdk-go-v2/service/mq"
|
||||
"github.com/aws/aws-sdk-go-v2/service/opensearch"
|
||||
"github.com/aws/aws-sdk-go-v2/service/organizations"
|
||||
@@ -156,7 +157,7 @@ var (
|
||||
DatabasesCommand = &cobra.Command{
|
||||
Use: "databases",
|
||||
Aliases: []string{"db", "rds", "redshift", "dbs"},
|
||||
Short: "Enumerate RDS databases. Get a loot file with connection strings.",
|
||||
Short: "Enumerate databases. Get a loot file with connection strings.",
|
||||
Long: "\nUse case examples:\n" +
|
||||
os.Args[0] + " aws databases --profile readonly_profile",
|
||||
PreRun: awsPreRun,
|
||||
@@ -448,7 +449,7 @@ var (
|
||||
|
||||
Use: "pmapper",
|
||||
Aliases: []string{"Pmapper", "pmapperParse"},
|
||||
Short: "",
|
||||
Short: "Looks for pmapper data for the account and builds a PrivEsc graph in golang if it exists.",
|
||||
Long: "\nUse case examples:\n" +
|
||||
os.Args[0] + " aws ",
|
||||
PreRun: awsPreRun,
|
||||
@@ -741,6 +742,7 @@ func runDatabasesCommand(cmd *cobra.Command, args []string) {
|
||||
RedshiftClient: redshift.NewFromConfig(AWSConfig),
|
||||
DynamoDBClient: dynamodb.NewFromConfig(AWSConfig),
|
||||
DocDBClient: docdb.NewFromConfig(AWSConfig),
|
||||
NeptuneClient: neptune.NewFromConfig(AWSConfig),
|
||||
Caller: *caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
|
||||
AWSProfile: profile,
|
||||
@@ -927,13 +929,13 @@ func runIamSimulatorCommand(cmd *cobra.Command, args []string) {
|
||||
continue
|
||||
}
|
||||
m := aws.IamSimulatorModule{
|
||||
IAMClient: iam.NewFromConfig(AWSConfig),
|
||||
Caller: *caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
IAMClient: iam.NewFromConfig(AWSConfig),
|
||||
Caller: *caller,
|
||||
AWSProfileProvided: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
}
|
||||
m.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputDirectory, Verbosity)
|
||||
}
|
||||
@@ -1169,19 +1171,21 @@ func runRAMCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
func runResourceTrustsCommand(cmd *cobra.Command, args []string) {
|
||||
for _, profile := range AWSProfiles {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)
|
||||
caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m := aws.ResourceTrustsModule{
|
||||
Caller: *caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
|
||||
WrapTable: AWSWrapTable,
|
||||
CloudFoxVersion: cmd.Root().Version,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
Caller: *caller,
|
||||
AWSProfileProvided: profile,
|
||||
Goroutines: Goroutines,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
|
||||
WrapTable: AWSWrapTable,
|
||||
CloudFoxVersion: cmd.Root().Version,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
AWSConfig: AWSConfig,
|
||||
}
|
||||
m.PrintResources(AWSOutputDirectory, Verbosity)
|
||||
}
|
||||
@@ -1800,15 +1804,16 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
|
||||
cloudFoxSNSClient.PrintSNS(AWSOutputDirectory, Verbosity)
|
||||
|
||||
resourceTrustsCommand := aws.ResourceTrustsModule{
|
||||
Caller: *caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
|
||||
WrapTable: AWSWrapTable,
|
||||
CloudFoxVersion: cmd.Root().Version,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
AWSMFAToken: AWSMFAToken,
|
||||
Caller: *caller,
|
||||
AWSProfileProvided: profile,
|
||||
Goroutines: Goroutines,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
|
||||
WrapTable: AWSWrapTable,
|
||||
CloudFoxVersion: cmd.Root().Version,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
AWSMFAToken: AWSMFAToken,
|
||||
AWSConfig: AWSConfig,
|
||||
}
|
||||
resourceTrustsCommand.PrintResources(AWSOutputDirectory, Verbosity)
|
||||
|
||||
@@ -1878,16 +1883,33 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
|
||||
pmapperCommand.PrintPmapperData(AWSOutputDirectory, Verbosity)
|
||||
|
||||
iamSimulator := aws.IamSimulatorModule{
|
||||
IAMClient: iamClient,
|
||||
Caller: *caller,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
IAMClient: iamClient,
|
||||
Caller: *caller,
|
||||
AWSProfileProvided: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
}
|
||||
iamSimulator.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputDirectory, Verbosity)
|
||||
|
||||
workloads := aws.WorkloadsModule{
|
||||
ECSClient: ecsClient,
|
||||
EC2Client: ec2Client,
|
||||
LambdaClient: lambdaClient,
|
||||
AppRunnerClient: appRunnerClient,
|
||||
IAMClient: iamClient,
|
||||
Caller: *caller,
|
||||
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
|
||||
SkipAdminCheck: AWSSkipAdminCheck,
|
||||
AWSProfile: profile,
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
AWSOutputType: AWSOutputType,
|
||||
AWSTableCols: AWSTableCols,
|
||||
}
|
||||
workloads.PrintWorkloads(AWSOutputDirectory, Verbosity)
|
||||
|
||||
fmt.Printf("[%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("That's it! Check your output files for situational awareness and check your loot files for next steps."))
|
||||
fmt.Printf("[%s] %s\n\n", cyan(emoji.Sprintf(":fox:cloudfox :fox:")), green("FYI, we skipped the outbound-assumed-roles module in all-checks (really long run time). Make sure to try it out manually."))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/BishopFox/cloudfox
|
||||
|
||||
go 1.20
|
||||
go 1.21.2
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
|
||||
@@ -10,7 +10,7 @@ require (
|
||||
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.24.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.2
|
||||
github.com/aws/aws-sdk-go-v2/service/apigateway v1.21.6
|
||||
github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.18.6
|
||||
@@ -63,7 +63,12 @@ require (
|
||||
golang.org/x/crypto v0.17.0
|
||||
)
|
||||
|
||||
require github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/service/neptune v1.28.2 // indirect
|
||||
github.com/bishopfox/knownawsaccountslookup v0.0.0-20231228165844-c37ef8df33cb // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect
|
||||
@@ -81,8 +86,8 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.13
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/athena v1.37.3
|
||||
@@ -96,7 +101,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.24.6
|
||||
github.com/aws/aws-sdk-go-v2/service/sfn v1.24.6
|
||||
|
||||
@@ -48,6 +48,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
|
||||
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.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.2 h1:+RWLEIWQIGgrz2pBPAUoGgNGs1TOyF4Hml7hCnYj2jc=
|
||||
@@ -58,8 +60,12 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6Jk
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw=
|
||||
@@ -130,6 +136,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.10 h1:h8uw
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.10/go.mod h1:LZKVtMBiZfdvUWgwg61Qo6kyAmE5rn9Dw36AqnycvG8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.24.6 h1:FO/aIHk86VePDUh/3Q/A5pnvu45miO1GZB8rIq2BUlA=
|
||||
@@ -140,6 +148,8 @@ github.com/aws/aws-sdk-go-v2/service/lightsail v1.32.5 h1:0KVnA62WGcVdeJKH+DTUkx
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.32.5/go.mod h1:wI7palPB84YaqCYglfNiyAlDcXTFbcJ9rDHMu15cFto=
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.20.6 h1:n86T5yw0kS6a5nbpkEpDzLPCBXXb35lx3iDkmQWlizA=
|
||||
github.com/aws/aws-sdk-go-v2/service/mq v1.20.6/go.mod h1:phfKOOpMQhlBv2KE8gF17P82zLcSedA9b7fMSGTLBdQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/neptune v1.28.2 h1:DOv1kEXarl5GGLxUKr4JSGB10gIEFi/XxDIJHOFAEss=
|
||||
github.com/aws/aws-sdk-go-v2/service/neptune v1.28.2/go.mod h1:blUZTHNHhlAi31tve04hxwROjCFoLt3HXM9d5ftliR0=
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.27.1 h1:qmxbwQY/gnN7eBElvy9saejXWaKD6ENU8JF5SuoAWLQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/opensearch v1.27.1/go.mod h1:l89g2sqHGQRg0UZ1ldFfOxh96mZAGn8ZBlaXpoJ2psQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/organizations v1.23.5 h1:4sW8XPTtuH6PX8CUcpUxBKg0Pf67k1MOOgq9Y+v4ls8=
|
||||
@@ -178,6 +188,8 @@ github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
||||
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||
github.com/bishopfox/awsservicemap v1.0.3 h1:0T+mJLwG+vQV9+o3dzwzxhWJWE40VpoCLWtaPBwixYc=
|
||||
github.com/bishopfox/awsservicemap v1.0.3/go.mod h1:oy9Fyqh6AozQjShSx+zRNouTlp7k3z3YEMoFkN8rquc=
|
||||
github.com/bishopfox/knownawsaccountslookup v0.0.0-20231228165844-c37ef8df33cb h1:ot96tC/kdm0GKV1kl+aXJorqJbyx92R9bjRQvbBmLKU=
|
||||
github.com/bishopfox/knownawsaccountslookup v0.0.0-20231228165844-c37ef8df33cb/go.mod h1:2OnSqu4B86+2xGSIE5D4z3Rze9yJ/LNNjNXHhwMR+vY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -317,6 +329,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+10
-3
@@ -60,15 +60,22 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
TxtLog.Println(err)
|
||||
//fmt.Println(err)
|
||||
if AWSProfile != "" {
|
||||
TxtLog.Println(err)
|
||||
fmt.Printf("[%s][%s] The specified profile [%s] does not exist or there was an error loading the credentials.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile), AWSProfile)
|
||||
TxtLog.Fatalf("Could not retrieve the specified profile name %s", err)
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile))
|
||||
TxtLog.Fatalf("Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile))
|
||||
}
|
||||
//os.Exit(1)
|
||||
}
|
||||
|
||||
_, err := cfg.Credentials.Retrieve(context.TODO())
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("[%s][%s] Error retrieving credentials from environment variables, or the instance metadata service.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), cyan(AWSProfile))
|
||||
TxtLog.Printf("Could not retrieve the specified profile name %s", err)
|
||||
|
||||
} else {
|
||||
// update the config map with the new config for future lookups
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
)
|
||||
|
||||
type TrustPolicyDocument struct {
|
||||
Version string `json:"Version"`
|
||||
Statement []RoleTrustStatementEntry `json:"Statement"`
|
||||
}
|
||||
|
||||
type RoleTrustStatementEntry struct {
|
||||
Sid string `json:"Sid"`
|
||||
Effect string `json:"Effect"`
|
||||
Principal struct {
|
||||
AWS ListOfPrincipals `json:"AWS"`
|
||||
Service ListOfPrincipals `json:"Service"`
|
||||
Federated ListOfPrincipals `json:"Federated"`
|
||||
} `json:"Principal"`
|
||||
Action string `json:"Action"`
|
||||
Condition struct {
|
||||
StringEquals struct {
|
||||
StsExternalID string `json:"sts:ExternalId"`
|
||||
SAMLAud string `json:"SAML:aud"`
|
||||
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
|
||||
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
|
||||
OidcEksSub ListOfPrincipals `json:"OidcEksSub"`
|
||||
OidcEksAud string `json:"OidcEksAud"`
|
||||
CognitoAud string `json:"cognito-identity.amazonaws.com:aud"`
|
||||
TerraformAud string `json:"app.terraform.io:aud"` // Terraform Cloud specific
|
||||
TerraformSub ListOfPrincipals `json:"app.terraform.io:sub"` // Terraform Cloud specific
|
||||
GCPAud string `json:"accounts.google.com:aud"`
|
||||
GCPSub ListOfPrincipals `json:"accounts.google.com:sub"`
|
||||
AzureADIss ListOfPrincipals `json:"http://sts.windows.net/tenant-id/iss"` // Azure AD specific
|
||||
AzureADSub ListOfPrincipals `json:"sub"` // Common among OIDC providers
|
||||
PingClientId string `json:"pingidentity.com:client_id"`
|
||||
GoogleWorkspaceAud string `json:"workspace.google.com:aud"`
|
||||
GoogleWorkspaceSub ListOfPrincipals `json:"workspace.google.com:sub"`
|
||||
CircleCIAud ListOfPrincipals `json:"CircleCIAud"`
|
||||
CircleCISub ListOfPrincipals `json:"CircleCISub"`
|
||||
// Add other provider-specific claims here
|
||||
} `json:"StringEquals"`
|
||||
StringLike struct {
|
||||
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
|
||||
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
|
||||
OidcEksSub ListOfPrincipals `json:"OidcEksSub"`
|
||||
OidcEksAud string `json:"OidcEksAud"`
|
||||
TerraformAud string `json:"app.terraform.io:aud"` // Terraform Cloud specific
|
||||
TerraformSub ListOfPrincipals `json:"app.terraform.io:sub"` // Terraform Cloud specific
|
||||
GCPAud string `json:"accounts.google.com:aud"`
|
||||
GCPSub ListOfPrincipals `json:"accounts.google.com:sub"`
|
||||
AzureADIss ListOfPrincipals `json:"http://sts.windows.net/tenant-id/iss"` // Azure AD specific
|
||||
AzureADSub ListOfPrincipals `json:"sub"` // Common among OIDC providers
|
||||
PingClientId string `json:"pingidentity.com:client_id"`
|
||||
GoogleWorkspaceAud string `json:"workspace.google.com:aud"`
|
||||
GoogleWorkspaceSub ListOfPrincipals `json:"workspace.google.com:sub"`
|
||||
CircleCIAud ListOfPrincipals `json:"CircleCIAud"`
|
||||
CircleCISub ListOfPrincipals `json:"CircleCISub"`
|
||||
// Add patterns for provider-specific claims that support wildcards or partial matches
|
||||
} `json:"StringLike"`
|
||||
ForAnyValueStringLike struct {
|
||||
CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"`
|
||||
//Auth0Amr ListOfPrincipals `json:"Auth0Amr"`
|
||||
// This section can be extended with any-value-like conditions for other providers if they support such conditions
|
||||
} `json:"ForAnyValue:StringLike"`
|
||||
} `json:"Condition"`
|
||||
}
|
||||
|
||||
// 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 ListOfPrincipals []string
|
||||
|
||||
func (r *ListOfPrincipals) 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")
|
||||
}
|
||||
|
||||
func ParseRoleTrustPolicyDocument(role types.Role) (TrustPolicyDocument, error) {
|
||||
document, _ := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument))
|
||||
|
||||
// These next six lines are a hack, needed because the EKS OIDC json field name is dynamic
|
||||
// and therefore can't be used to unmarshall in a predictable way. The hack involves replacing
|
||||
// the random pattern with a predictable one so that we can add the predictable one in the struct
|
||||
// used to unmarshall.
|
||||
pattern := `(\w+)\:`
|
||||
pattern2 := `".[a-zA-Z0-9\-\.]+/id/`
|
||||
//auth0Pattern := `"auth0.com\:`
|
||||
circleCIPattern := `"oidc.circleci.com/org/[a-zA-Z0-9\-]+:`
|
||||
var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub")
|
||||
var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud")
|
||||
//var reAuth0Sub = regexp.MustCompile(auth0Pattern + "amr")
|
||||
var reCircleCIAud = regexp.MustCompile(circleCIPattern + "aud")
|
||||
var reCircleCISSub = regexp.MustCompile(circleCIPattern + "sub")
|
||||
|
||||
document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub")
|
||||
document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud")
|
||||
//document = reAuth0Sub.ReplaceAllString(document, "\"Auth0Amr")
|
||||
document = reCircleCIAud.ReplaceAllString(document, "\"CircleCIAud")
|
||||
document = reCircleCISSub.ReplaceAllString(document, "\"CircleCISub")
|
||||
|
||||
var parsedDocumentToJSON TrustPolicyDocument
|
||||
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
|
||||
return parsedDocumentToJSON, nil
|
||||
}
|
||||
+8
-6
@@ -144,8 +144,11 @@ func (l *LootClient) createLootFiles(lootFiles []LootFile) {
|
||||
if l.DirectoryName == "" {
|
||||
l.DirectoryName = "."
|
||||
}
|
||||
if _, err := fileSystem.Stat(l.DirectoryName); os.IsNotExist(err) {
|
||||
err = fileSystem.MkdirAll(l.DirectoryName, 0700)
|
||||
|
||||
lootDirectory := path.Join(l.DirectoryName, "loot")
|
||||
|
||||
if _, err := fileSystem.Stat(lootDirectory); os.IsNotExist(err) {
|
||||
err = fileSystem.MkdirAll(lootDirectory, 0700)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -156,7 +159,7 @@ func (l *LootClient) createLootFiles(lootFiles []LootFile) {
|
||||
|
||||
l.LootFiles[i].Name = fmt.Sprintf("%s.txt", file.Name)
|
||||
|
||||
filePointer, err := fileSystem.OpenFile(path.Join(l.DirectoryName, l.LootFiles[i].Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
filePointer, err := fileSystem.OpenFile(path.Join(lootDirectory, l.LootFiles[i].Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating output file: %s", err)
|
||||
}
|
||||
@@ -169,9 +172,8 @@ func (l *LootClient) writeLootFiles() []string {
|
||||
var fullFilePaths []string
|
||||
for _, file := range l.LootFiles {
|
||||
contents := []byte(file.Contents)
|
||||
fullPath := path.Join(l.DirectoryName, file.Name)
|
||||
|
||||
err := os.WriteFile(fullPath, contents, 0644)
|
||||
fullPath := path.Join(l.DirectoryName, "loot", file.Name)
|
||||
err := afero.WriteFile(fileSystem, fullPath, contents, 0644) // Use Afero's WriteFile
|
||||
if err != nil {
|
||||
log.Fatalf("error writing loot file %s: %s", file.Name, err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user