Compare commits

..
Author SHA1 Message Date
David db7091f33b Merge branch 'main' into general-improvements_azure-commands 2024-01-30 11:48:37 -05:00
Bastien Faure 465f08b530 Azure netscan: reduce list of network targets by filtering running vms, included in target subnets 2023-11-08 16:22:53 -08:00
Bastien Faure 8da636e294 Detach a few VM functions from VMs module and make them shared 2023-11-08 16:20:33 -08:00
Bastien Faure 6c7c26a53f Properly display ASG in NSG rules 2023-11-08 16:19:44 -08:00
Bastien Faure 2086a72275 Azure netscan: write scan file into loot directory 2023-11-05 22:32:17 -08:00
Bastien Faure ab6806d641 Added draft Azure netscan command 2023-11-05 22:13:34 -08:00
Bastien Faure 3e3608f774 Merge branch 'general-improvements_azure-commands' of ssh://bf.github.com/BishopFox/cloudfox into general-improvements_azure-commands 2023-11-01 10:27:39 -07:00
Bastien Faure 7941a2c712 Missing panic call in the logger 2023-11-01 10:25:33 -07:00
Bastien Faure 67feb23ad2 Allowed 'expand' param to get passed to NIC getter 2023-11-01 10:24:50 -07:00
Bastien Faure 14bcd43158 Address potential nil pointer deref, wrong module name in NSG rules 2023-11-01 10:24:07 -07:00
Bastien Faure 0d3acc1006 Codespell fix 2023-10-31 21:43:48 -07:00
Bastien Faure f371bc7021 Removed legacy Azure CLI flags and introduced repeatable ones 2023-10-31 19:03:14 -07:00
Bastien Faure 272bc0ad11 Removed additional legacy code from the Azure Storage module 2023-10-31 18:57:34 -07:00
Bastien Faure 3cf38defdd Fixed wrong output directory for Azure whoami module 2023-10-31 18:44:59 -07:00
Bastien Faure 6725aaaf45 Update README with new Azure commands 2023-10-31 18:38:47 -07:00
Bastien Faure b0fe4a290f Migrated all Azure modules to new logging system, disabled legacy subscription and tenant iteration mechanisms 2023-10-31 17:59:02 -07:00
Bastien Faure 20881183d1 Migrated Azure NSG modules to new logger and eliminated legacy tenant and subscription ID manipulation 2023-10-31 17:15:21 -07:00
Bastien Faure 8d989ccaf9 Migrature Azure inventory module to new logger and eliminated legacy tenant and subscription ID manipulation 2023-10-31 16:57:35 -07:00
Bastien Faure a91e0a891e Migrated Azure storage module to use the new logger, as well as the Azure root module 2023-10-31 16:38:05 -07:00
Bastien Faure 386f3cb059 Implemented custom logger usable by any module and cloud provider 2023-10-31 16:37:22 -07:00
Bastien Faure effb6a490e Moved version information to a file and leverage go:embed 2023-10-31 16:36:56 -07:00
Bastien Faure f6f78efd42 Moved version information to its own file 2023-10-30 22:47:29 -07:00
Bastien Faure 924fd3d89e Merge remote-tracking branch 'origin/main' into general-improvements_azure-commands 2023-10-30 21:17:05 -07:00
Bastien Faure 60dbe425a5 Cosmetic fixes in the NSG modules 2023-10-30 21:16:36 -07:00
Bastien Faure bffa980d22 Added the two new Azure nsg-links and nsg-rules commands, added a map[string][]subscription populated prior to run any module 2023-10-30 21:01:08 -07:00
Bastien Faure 6665a9a5f0 Migrated the Azure RBAC command to a fully modular structure 2023-10-30 16:00:18 -07:00
Bastien Faure 50445263d7 Migrated the Azure storage command to a fully modular structure, implemented RG filtering 2023-10-30 15:36:40 -07:00
Bastien Faure ec422dcc6c Migrated the Azure VMs command to a fully modular structure, implemented RG filtering 2023-10-30 15:12:24 -07:00
Bastien Faure 416c93fdf1 Skip PreRun when running Azure Whoami module, but better 2023-10-30 13:34:38 -07:00
Bastien Faure 9bd96cfb66 Skip PreRun when running Azure Whoami module 2023-10-30 13:16:44 -07:00
Bastien Faure b38bf98072 Migrated the Azure Inventory command to a fully modular structure, implemented RG filtering 2023-10-30 12:49:21 -07:00
Bastien Faure 7e209883fb Fix wrong reference to resource ID in Azure RBAC and VMs modules 2023-10-30 12:25:18 -07:00
Bastien Faure 991decf2d9 Code cleanup in Azure modules 2023-10-29 02:08:20 -07:00
Bastien Faure 14132b3958 Migrated Azure storage command to new format 2023-10-29 02:04:47 -07:00
Bastien Faure 325b6ce892 Migrated Azure NSG commands to new format 2023-10-29 02:00:12 -07:00
Bastien Faure b7cca44509 Migrated Azure RBAC subcommand to new format 2023-10-29 01:50:13 -07:00
Bastien Faure c2100cd927 Migrated Azure VMs command to new format and fixed tenant reference bug in Inventory 2023-10-29 01:40:04 -07:00
Bastien Faure 71a60a6e67 Migrated Azure Inventory command to new format 2023-10-29 01:16:42 -07:00
Bastien Faure 04db8765a9 Implemented AzureClient class that autopopulates for all subcommands, moved some code to internal package 2023-10-29 01:12:29 -07:00
Bastien Faure e5de75bd07 Started to prepare new resource selection logic for the Azure subcommand 2023-10-29 00:01:24 -07:00
Bastien Faure 5b5a69a605 Migrated AWS CLI structure into Azure CLI 2023-10-28 18:17:27 -07:00
Bastien Faure 783524ba9c Added skeleton for Azure nsg-rules command 2023-10-28 17:45:00 -07:00
Bastien Faure 7410dab4f6 Added Azure nsg-links command to dump NSG attachments 2023-10-28 17:43:11 -07:00
67 changed files with 1863 additions and 4194 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
* @bishopfaure @dbravo-bishopfox
@sethsec-bf carlosvendramini-bf
-1
View File
@@ -15,7 +15,6 @@
# Dependency directories (remove the comment below to include it)
# vendor/
.vscode
.idea
launch.json
######################################################
+5 -2
View File
@@ -48,8 +48,8 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
| Provider| CloudFox Commands |
| - | - |
| AWS | 33 |
| Azure | 4 |
| AWS | 30 |
| Azure | 6 |
| GCP | Support Planned |
| Kubernetes | Support Planned |
@@ -149,11 +149,14 @@ Additional policy notes (as of 09/2022):
| 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 | [nsg-links](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#nsg-links) | Lists subnets and network interfaces attached to Network Security Groups |
| Azure | [nsg-rules](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#nsg-rules) | Lists rules in Network Security Groups |
| 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)
* [Seth Art (@sethsec](https://twitter.com/sethsec))
* [Bastien Faure](https://github.com/BastienFaure)
# Contributing
[Wiki - How to Contribute](https://github.com/BishopFox/cloudfox/wiki#how-to-contribute)
+8 -1
View File
@@ -2,6 +2,7 @@ package aws
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
@@ -143,7 +144,7 @@ func (m *ApiGwModule) PrintApiGws(outputDirectory string, verbosity int) {
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
loot := m.writeLoot(filepath, verbosity)
loot := m.writeLoot(o.Table.DirectoryName, verbosity)
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
Name: m.output.CallingModule,
Contents: loot,
@@ -206,6 +207,12 @@ func (m *ApiGwModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan
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
+1 -3
View File
@@ -9,7 +9,6 @@ import (
"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"
)
@@ -30,8 +29,7 @@ func TestApiGw(t *testing.T) {
fs := internal.MockFileSystem(true)
defer internal.MockFileSystem(false)
//tmpDir := "~/.cloudfox/"
tmpDir := ptr.ToString(internal.GetLogDirPath())
tmpDir := "~/.cloudfox/"
m.PrintApiGws(tmpDir, 2)
+130 -141
View File
@@ -3,6 +3,7 @@ package aws
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
@@ -21,6 +22,7 @@ type DatabasesModule struct {
RDSClient sdk.RDSClientInterface
RedshiftClient sdk.AWSRedShiftClientInterface
DynamoDBClient sdk.DynamoDBClientInterface
DocDBClient sdk.DocDBClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
@@ -51,7 +53,6 @@ type Database struct {
Protocol string
Public string
Size string
Roles string
}
func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int) {
@@ -67,13 +68,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, Redshift, DynamoDB, DocumentDB, Neptune\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
fmt.Printf("[%s][%s] Supported Services: RDS, DynamoDB\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
wg := new(sync.WaitGroup)
semaphore := make(chan struct{}, m.Goroutines)
// Create a channel to signal the spinner aka task status goroutine to finish
spinnerDone := make(chan bool)
//fire up the task status spinner/updated
//fire up the the task status spinner/updated
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
//create a channel to receive the objects
@@ -111,8 +112,7 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
"Size",
"UserName",
"Endpoint",
"Port",
"Roles",
//"Port",
//"Protocol",
//"Public",
@@ -139,8 +139,6 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
"Size",
"UserName",
"Endpoint",
"Port",
"Roles",
}
// Otherwise, use the default columns.
} else {
@@ -152,7 +150,6 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
"Size",
"UserName",
"Endpoint",
"Port",
}
}
@@ -169,8 +166,7 @@ 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)),
m.Databases[i].Roles,
// strconv.Itoa(int(m.Databases[i].Port)),
// m.Databases[i].Protocol,
// m.Databases[i].Public,
},
@@ -178,17 +174,13 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
}
if len(m.output.Body) > 0 {
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.AWSProfile, aws.ToString(m.Caller.Account)))
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
Table: internal.TableClient{
Wrap: m.WrapTable,
DirectoryName: filepath,
},
Loot: internal.LootClient{
DirectoryName: filepath,
Wrap: m.WrapTable,
},
}
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
@@ -198,15 +190,9 @@ func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int)
Name: m.output.CallingModule,
})
o.PrefixIdentifier = m.AWSProfile
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)
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s databases found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
} else {
fmt.Printf("[%s][%s] No databases found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
@@ -231,74 +217,56 @@ 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",
}
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)
}
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)
res, err := servicemap.IsServiceInRegion("rds", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
check.wg.Add(1)
go check.executor(check.region, check.wg, check.semaphore, check.dataReceiver)
wg.Add(1)
go m.getRdsClustersPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("redshift", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getRedshiftDatabasesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("dynamodb", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getDynamoDBTablesPerRegion(r, wg, semaphore, dataReceiver)
}
res, err = servicemap.IsServiceInRegion("docdb", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getDocDBTablesPerRegion(r, wg, semaphore, dataReceiver)
}
}
func (m *DatabasesModule) 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) writeLoot(outputDirectory string, verbosity int) string {
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) {
path := filepath.Join(outputDirectory, "loot")
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
panic(err.Error())
}
f := filepath.Join(path, "databases-UrlsOnly.txt")
var out string
@@ -307,12 +275,12 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) strin
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()
@@ -323,8 +291,6 @@ func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) strin
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) {
@@ -341,58 +307,45 @@ func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup,
m.CommandCounter.Pending--
m.CommandCounter.Executing++
DBClusters, err := sdk.CachedRDSDescribeDBClusters(m.RDSClient, aws.ToString(m.Caller.Account), r)
DBInstances, err := sdk.CachedRDSDescribeDBInstances(m.RDSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
if errors.As(err, &oe) {
m.Errors = append(m.Errors, (fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation())))
}
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, cluster := range DBClusters {
var public string
var service string
var roles string
if cluster.Endpoint == nil {
continue
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,
}
}
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) {
@@ -416,7 +369,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++
@@ -428,19 +381,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: port,
Port: aws.ToInt32(port),
Protocol: protocol,
Public: public,
}
@@ -489,12 +442,48 @@ func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGrou
Endpoint: "N/A",
}
}
}
func isNeptune(engine *string) bool {
return *engine == "neptune"
}
func (m *DatabasesModule) getDocDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()
func isDocDB(engine *string) bool {
return *engine == "docdb"
}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()
// m.CommandCounter.Total++
m.CommandCounter.Pending--
m.CommandCounter.Executing++
awsService := "DocDB"
Clusters, err := sdk.CachedDocDBDescribeDBClusters(m.DocDBClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
return
}
for _, cluster := range Clusters {
name := aws.ToString(cluster.DBClusterIdentifier)
endpoint := aws.ToString(cluster.Endpoint)
port := aws.ToInt32(cluster.Port)
//size := aws.ToInt64(TableOutput.Table.TableSizeBytes)
userName := aws.ToString(cluster.MasterUsername)
dataReceiver <- Database{
AWSService: awsService,
Region: r,
Name: name,
Endpoint: endpoint,
Port: port,
UserName: userName,
//Size: strconv.Itoa(int(size)),
}
}
}
-57
View File
@@ -1,57 +0,0 @@
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{},
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
View File
@@ -836,7 +836,7 @@ func (m *EnvsModule) interestingEnvVarsOnly() [][]string {
for _, envVar := range m.EnvironmentVariables {
if envVar.interesting {
interestingBody = append(
interestingBody, []string{
m.output.Body, []string{
aws.ToString(m.Caller.Account),
envVar.service,
envVar.region,
+148 -150
View File
@@ -1,17 +1,21 @@
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"
@@ -61,12 +65,46 @@ type RoleTrustRow struct {
type AnalyzedRole struct {
roleARN *string
trustsDoc policy.TrustPolicyDocument
trustsDoc trustPolicyDocument
// trustType string // UNUSED FIELD, PLEASE REVIEW
Admin string
CanPrivEsc string
}
type trustPolicyDocument struct {
Version string `json:"Version"`
Statement []RoleTrustStatementEntry `json:"Statement"`
}
type RoleTrustStatementEntry struct {
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Principal struct {
AWS ListOfPrincipals `json:"AWS"`
Service ListOfPrincipals `json:"Service"`
Federated ListOfPrincipals `json:"Federated"`
} `json:"Principal"`
Action string `json:"Action"`
Condition struct {
StringEquals struct {
StsExternalID string `json:"sts:ExternalId"`
SAMLAud string `json:"SAML:aud"`
OidcEksSub string `json:"OidcEksSub"`
OidcEksAud string `json:"OidcEksAud"`
CognitoAud string `json:"cognito-identity.amazonaws.com:aud"`
} `json:"StringEquals"`
StringLike struct {
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
OidcEksSub string `json:"OidcEksSub"`
OidcEksAud string `json:"OidcEksAud"`
} `json:"StringLike"`
ForAnyValueStringLike struct {
CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"`
} `json:"ForAnyValue:StringLike"`
} `json:"Condition"`
}
func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int) {
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
@@ -102,7 +140,6 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
}
}
o := internal.OutputClient{
Verbosity: verbosity,
CallingModule: m.output.CallingModule,
@@ -152,12 +189,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 service role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
if len(federatedBody) > 0 {
fmt.Printf("[%s][%s] %s federated role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
} else {
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
}
@@ -204,34 +241,33 @@ 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") {
if strings.Contains(principal, ":root") {
//check to see if the accountID is known
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),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedPrincipal,
RoleTrustRow.ExternalID,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedPrincipal,
RoleTrustRow.ExternalID,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
}
m.sortTrustsTablePerTrustedPrincipal()
@@ -336,7 +372,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{"Account", "Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
tableCols = []string{"Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
// Otherwise, use the default columns for this module (brief)
} else {
tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
@@ -404,7 +440,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{"Account", "Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
tableCols = []string{"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?"}
@@ -417,25 +453,23 @@ 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, 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})
provider, subject := parseFederatedTrustPolicy(statement)
RoleTrustRow := RoleTrustRow{
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedFederatedProvider: provider,
TrustedFederatedSubject: subject,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
RoleTrustRow.RoleARN,
RoleTrustRow.RoleName,
RoleTrustRow.TrustedFederatedProvider,
RoleTrustRow.TrustedFederatedSubject,
RoleTrustRow.IsAdmin,
RoleTrustRow.CanPrivEsc})
}
}
@@ -446,120 +480,47 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
}
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...)
func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) {
var column2, column3 string
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")"
trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n")
if trustedRepos == "" {
column3 = "ALL REPOS!!!"
} else {
subjects = append(subjects, "ALL REPOS!!!")
column3 = trustedRepos
}
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 if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
column2 = "Okta" // (" + statement.Principal.Federated[0] + ")"
}
column3 = "Not applicable"
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" {
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
if statement.Condition.StringEquals.OidcEksSub != "" {
column3 = statement.Condition.StringEquals.OidcEksSub
} else if statement.Condition.StringLike.OidcEksSub != "" {
column3 = statement.Condition.StringLike.OidcEksSub
} else {
subjects = append(subjects, "ALL SERVICE ACCOUNTS!!!")
column3 = "ALL SERVICE ACCOUNTS!"
}
// 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")
}
// 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"
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
column2 = "Cognito" // (" + statement.Principal.Federated[0] + ")"
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
subjects = append(subjects, statement.Condition.ForAnyValueStringLike.CognitoAMR)
} else {
subjects = append(subjects, "ALL IDENTITIES")
column3 = statement.Condition.ForAnyValueStringLike.CognitoAMR
}
// 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")
} 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] + ")"
}
// 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 provider, subjects
return column2, column3
}
func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
@@ -579,7 +540,7 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
}
for _, role := range ListRoles {
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
trustsdoc, err := parseRoleTrustPolicyDocument(role)
if err != nil {
m.modLog.Error(err.Error())
m.CommandCounter.Error++
@@ -599,3 +560,40 @@ 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")
}
+14 -96
View File
@@ -9,7 +9,6 @@ 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 {
@@ -23,32 +22,13 @@ 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(CachedGetStagesOutput{})
gob.Register(apigateway.GetStagesOutput{})
gob.Register([]apiGatewayTypes.Resource{})
gob.Register([]apiGatewayTypes.DomainName{})
gob.Register([]apiGatewayTypes.BasePathMapping{})
gob.Register(CachedGetMethodOutput{})
gob.Register(apigateway.GetMethodOutput{})
gob.Register([]apiGatewayTypes.UsagePlan{})
gob.Register([]apiGatewayTypes.UsagePlanKey{})
}
@@ -86,18 +66,16 @@ 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 {
// Convert cached data back to GetStagesOutput before returning
cachedOutput := cached.(*CachedGetStagesOutput) // Ensure this type assertion matches your caching logic
return fromCachedGetStagesOutput(cachedOutput), nil
return cached.(apigateway.GetStagesOutput), nil
}
GetStages, err := client.GetStages(
@@ -111,28 +89,10 @@ func CachedApiGatewayGetStages(client APIGatewayClientInterface, accountID strin
)
if err != nil {
return &apigateway.GetStagesOutput{}, err
return apigateway.GetStagesOutput{}, 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
}
return *GetStages, err
}
// 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
@@ -169,7 +129,7 @@ func CachedApiGatewayGetResources(client APIGatewayClientInterface, accountID st
}
PaginationControl = GetResources.Position
}
internal.Cache.Set(cacheKey, resources, cache.DefaultExpiration)
return resources, nil
}
@@ -206,7 +166,7 @@ func CachedApiGatewayGetDomainNames(client APIGatewayClientInterface, accountID
}
PaginationControl = GetDomainNames.Position
}
internal.Cache.Set(cacheKey, domainNames, cache.DefaultExpiration)
return domainNames, nil
}
@@ -244,19 +204,17 @@ 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 {
// Convert cached data back to GetMethodOutput before returning
cachedOutput := cached.(*CachedGetMethodOutput) // Ensure this type assertion matches your caching logic
return fromCachedGetMethodOutput(cachedOutput), nil
return cached.(apigateway.GetMethodOutput), nil
}
GetMethod, err := client.GetMethod(
@@ -272,51 +230,13 @@ func CachedApiGatewayGetMethod(client APIGatewayClientInterface, accountID strin
)
if err != nil {
return &apigateway.GetMethodOutput{}, err
return apigateway.GetMethodOutput{}, err
}
// Convert GetMethodOutput to CachedGetMethodOutput before caching
cachedVersion := toCachedGetMethodOutput(GetMethod)
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
return GetMethod, nil
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
@@ -351,7 +271,6 @@ func CachedApiGatewayGetUsagePlans(client APIGatewayClientInterface, accountID s
PaginationControl = GetUsagePlans.Position
}
internal.Cache.Set(cacheKey, usagePlans, cache.DefaultExpiration)
return usagePlans, nil
}
@@ -390,6 +309,5 @@ func CachedApiGatewayGetUsagePlanKeys(client APIGatewayClientInterface, accountI
PaginationControl = GetUsagePlanKeys.Position
}
internal.Cache.Set(cacheKey, usagePlanKeys, cache.DefaultExpiration)
return usagePlanKeys, nil
}
+1 -2
View File
@@ -21,14 +21,13 @@ 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([]stage{})
gob.Register([]apiGatwayV2Types.Stage{})
gob.Register([]apiGatwayV2Types.Route{})
}
+1 -1
View File
@@ -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{})
}
+4 -6
View File
@@ -10,6 +10,10 @@ 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{
@@ -17,9 +21,3 @@ 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
}
-39
View File
@@ -14,13 +14,10 @@ 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) {
@@ -58,39 +55,3 @@ 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
}
+2 -52
View File
@@ -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,53 +32,3 @@ 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
}
+78 -74
View File
@@ -7,107 +7,101 @@ import (
"sort"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzInventoryCommand(AzTenantID, AzSubscriptionID, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
type AzInventoryModule struct {
AzClient *internal.AzureClient
Log *internal.Logger
}
func (m *AzInventoryModule) AzInventoryCommand() error {
o := internal.OutputClient{
Verbosity: AzVerbosity,
CallingModule: globals.AZ_INVENTORY_MODULE_NAME,
Verbosity: m.AzClient.AzVerbosity,
CallingModule: "inventory",
Table: internal.TableClient{
Wrap: AzWrapTable,
Wrap: m.AzClient.AzWrapTable,
},
}
if AzTenantID != "" && AzSubscriptionID == "" {
if len(m.AzClient.AzTenants) > 0 {
// cloudfox azure inventory --tenant [TENANT_ID | PRIMARY_DOMAIN]
tenantInfo := populateTenant(AzTenantID)
for _, AzTenant := range m.AzClient.AzTenants {
if AzMergedTable {
// set up table vars
var header []string
var body [][]string
if m.AzClient.AzMergedTable {
// set up table vars
var header []string
var body [][]string
o := internal.OutputClient{
Verbosity: AzVerbosity,
CallingModule: globals.AZ_INVENTORY_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
},
o := internal.OutputClient{
Verbosity: m.AzClient.AzVerbosity,
CallingModule: "inventory",
Table: internal.TableClient{
Wrap: m.AzClient.AzWrapTable,
},
}
m.Log.Infof(nil, "Gathering inventory for tenant %s (%s)", ptr.ToString(AzTenant.DefaultDomain), ptr.ToString(AzTenant.TenantID))
o.PrefixIdentifier = ptr.ToString(AzTenant.DefaultDomain)
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(AzTenant.DefaultDomain), "1-tenant-level")
//populate the table data
header, body, err := m.getInventoryInfoPerTenant(ptr.ToString(AzTenant.TenantID))
if err != nil {
return err
}
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(o.CallingModule)})
if body != nil {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
} else {
for _, AzSubscription := range GetSubscriptionsPerTenantID(ptr.ToString(AzTenant.TenantID)) {
m.runInventoryCommandForSingleSubscription(*AzTenant.DefaultDomain, &AzSubscription)
}
}
fmt.Printf(
"[%s][%s] Gathering inventory for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(o.CallingModule),
fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)))
o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain)
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level")
//populate the table data
header, body, err := getInventoryInfoPerTenant(ptr.ToString(tenantInfo.ID))
if err != nil {
return err
}
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(o.CallingModule)})
if body != nil {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
} else {
for _, s := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) {
runInventoryCommandForSingleSubscription(ptr.ToString(s.SubscriptionID), AzOutputDirectory, AzVerbosity, AzWrapTable, Version)
}
} else {
// ./cloudfox azure inventory --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME]
for tenantSlug, AzSubscriptions := range m.AzClient.AzSubscriptionsAlt {
for _, AzSubscription := range AzSubscriptions {
m.runInventoryCommandForSingleSubscription(tenantSlug, AzSubscription)
}
}
} else if AzTenantID == "" && AzSubscriptionID != "" {
// ./cloudfox azure inventory --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME]
runInventoryCommandForSingleSubscription(AzSubscriptionID, AzOutputDirectory, AzVerbosity, AzWrapTable, Version)
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
}
o.WriteFullOutput(o.Table.TableFiles, nil)
return nil
}
func runInventoryCommandForSingleSubscription(AzSubscription string, AzOutputDirectory string, AzVerbosity int, AzWrapTable bool, Version string) error {
func (m *AzInventoryModule) runInventoryCommandForSingleSubscription(tenantSlug string, AzSubscription *subscriptions.Subscription) error {
// set up table vars
var header []string
var body [][]string
var err error
o := internal.OutputClient{
Verbosity: AzVerbosity,
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_INVENTORY_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
Wrap: m.AzClient.AzWrapTable,
},
}
var AzSubscriptionInfo SubsriptionInfo
tenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription))
tenantInfo := populateTenant(tenantID)
AzSubscriptionInfo = PopulateSubsriptionType(AzSubscription)
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
o.PrefixIdentifier = *AzSubscription.DisplayName
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantSlug, *AzSubscription.DisplayName)
fmt.Printf(
"[%s][%s] Gathering inventory for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(o.CallingModule),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
m.Log.Infof(nil, "Gathering inventory for subscription %s (%s)", *AzSubscription.DisplayName, *AzSubscription.SubscriptionID)
// populate the table data
header, body, err = getInventoryInfoPerSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID)
header, body, err = m.getInventoryInfoPerSubscription(tenantSlug, AzSubscription)
if err != nil {
return err
}
@@ -126,8 +120,8 @@ func runInventoryCommandForSingleSubscription(AzSubscription string, AzOutputDir
return nil
}
func getInventoryInfoPerSubscription(tenantID, subscriptionID string) ([]string, [][]string, error) {
resources, err := getResources(tenantID, subscriptionID)
func (m *AzInventoryModule) getInventoryInfoPerSubscription(tenantSlug string, AzSubscription *subscriptions.Subscription) ([]string, [][]string, error) {
resources, err := m.getResources(*AzSubscription.TenantID, *AzSubscription.SubscriptionID)
if err != nil {
return nil, nil, err
}
@@ -139,14 +133,24 @@ func getInventoryInfoPerSubscription(tenantID, subscriptionID string) ([]string,
for _, resource := range resources {
resourceType := ptr.ToString(resource.Type)
resourceLocation := ptr.ToString(resource.Location)
_, ok := inventory[resourceType]
if len(m.AzClient.AzRGs) > 0 {
for _, AzRG := range m.AzClient.AzRGs {
metaResource, _ := azure.ParseResourceID(*resource.ID)
if metaResource.ResourceGroup == *AzRG.Name {
goto ADD_RESOURCE
}
}
goto SKIP_RESOURCE
}
ADD_RESOURCE:
if !ok {
inventory[resourceType] = make(map[string]int)
}
inventory[resourceType][resourceLocation]++
resourceTypes[resourceType] = true
resourceLocations[resourceLocation] = true
SKIP_RESOURCE:
}
header := []string{"Resource Type"}
@@ -173,14 +177,14 @@ func getInventoryInfoPerSubscription(tenantID, subscriptionID string) ([]string,
return header, body, nil
}
func getInventoryInfoPerTenant(tenantID string) ([]string, [][]string, error) {
func (m *AzInventoryModule) getInventoryInfoPerTenant(tenantID string) ([]string, [][]string, error) {
inventory := make(map[string]map[string]int)
resourceTypes := make(map[string]bool)
resourceLocations := make(map[string]bool)
for _, s := range GetSubscriptionsPerTenantID(tenantID) {
resources, err := getResources(tenantID, ptr.ToString(s.SubscriptionID))
resources, err := m.getResources(tenantID, ptr.ToString(s.SubscriptionID))
if err != nil {
return nil, nil, err
}
@@ -223,7 +227,7 @@ func getInventoryInfoPerTenant(tenantID string) ([]string, [][]string, error) {
return header, body, nil
}
func getResources(tenantID, subscriptionID string) ([]*armresources.GenericResourceExpanded, error) {
func (m *AzInventoryModule) getResources(tenantID, subscriptionID string) ([]*armresources.GenericResourceExpanded, error) {
client := internal.GetARMresourcesClient(tenantID, subscriptionID)
var resources []*armresources.GenericResourceExpanded
+248
View File
@@ -0,0 +1,248 @@
package azure
// https://learn.microsoft.com/en-us/azure/virtual-network/network-security-group-how-it-works
// https://learn.microsoft.com/en-us/azure/virtual-network/application-security-groups
// https://learn.microsoft.com/en-us/azure/network-watcher/diagnose-vm-network-traffic-filtering-problem?toc=%2Fazure%2Fvirtual-network%2Ftoc.json
import (
"github.com/BishopFox/cloudfox/internal"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
"github.com/Azure/go-autorest/autorest/azure"
"strings"
"github.com/aws/smithy-go/ptr"
"context"
"path/filepath"
"fmt"
"net"
"os"
)
type AzNetScanModule struct {
AzClient *internal.AzureClient
Log *internal.Logger
}
func (m *AzNetScanModule) AzNetScanCommand(sourceIP string) error {
o := internal.OutputClient{
Verbosity: m.AzClient.AzVerbosity,
CallingModule: "netscan",
Table: internal.TableClient{
Wrap: m.AzClient.AzWrapTable,
},
}
if len(m.AzClient.AzResources) == 0 && sourceIP == "" {
return fmt.Errorf("No resource ID or source IP address supplied")
}
if sourceIP != "" {
m.Log.Warnf(nil, "Searching resources attached to IP %s, this might take a while", sourceIP)
} else {
for _, AzResource := range m.AzClient.AzResources {
if AzResource.ResourceType == "virtualMachines" {
m.Log.Infof(nil, "Starting network analysis for Virtual Machine %s", AzResource.ResourceName)
m.processVM(AzResource.SubscriptionID, AzResource.ResourceGroup, AzResource.ResourceName)
} else {
m.Log.Errorf(nil, "Resource type %s / %s not implemented", AzResource.Provider, AzResource.ResourceType)
continue
}
}
}
o.WriteFullOutput(o.Table.TableFiles, nil)
return nil
}
func (m *AzNetScanModule) processVM(subscriptionID string, resourceGroupName string, VMName string) {
computeClient := internal.GetVirtualMachinesClient(subscriptionID)
vm, err := computeClient.Get(context.TODO(), resourceGroupName, VMName, "")
targetSubnets := []string{}
targetSubnetsAlt := make(map[string][]string)
vmSubscriptionIDs := make(map[string]bool)
vmSubscriptionIDs[subscriptionID] = true
if err != nil {
m.Log.Errorf([]string{VMName}, "Could not fetch virtual machine details")
return
} else {
if vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces != nil {
for _, nicReference := range *vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces {
nic, err := getNICdetails(subscriptionID, resourceGroupName, nicReference, "networkSecurityGroup")
if err != nil {
m.Log.Warnf([]string{VMName}, "Could not get details of network interface %s", *nicReference.ID)
continue
}
if nic.InterfacePropertiesFormat.IPConfigurations == nil {
m.Log.Warn([]string{VMName, *nic.Name}, "Interface has no IP address configured")
continue
}
if nic.InterfacePropertiesFormat.NetworkSecurityGroup != nil {
m.Log.Infof([]string{VMName, *nic.Name}, "Linked to NSG %s", *nic.InterfacePropertiesFormat.NetworkSecurityGroup.Name)
}
//var subnet *network.Subnet
for _, ip := range *nic.InterfacePropertiesFormat.IPConfigurations {
if ip.InterfaceIPConfigurationPropertiesFormat.PrivateIPAddress == nil {
m.Log.Warn([]string{VMName, *nic.Name}, "Empty IP configuration")
continue
}
ipConfig := ip.InterfaceIPConfigurationPropertiesFormat
m.Log.Infof([]string{VMName, *nic.Name}, "Interface assigned with address %s", *ipConfig.PrivateIPAddress)
subnet := *ipConfig.Subnet
subnetRGName := strings.Split(*subnet.ID, "/")[4]
VNETName := strings.Split(*subnet.ID, "/")[8]
subnetName := strings.Split(*subnet.ID, "/")[10]
subnet, err := getSubnetDetails(subscriptionID, subnetRGName, VNETName, subnetName, "networkSecurityGroup")
if err != nil {
m.Log.Warnf([]string{VMName, *nic.Name, *ipConfig.PrivateIPAddress}, "Unable to get subnet %s details", subnetName)
continue
}
if subnet.SubnetPropertiesFormat.NetworkSecurityGroup != nil {
m.Log.Infof([]string{VMName, *nic.Name, *ipConfig.PrivateIPAddress}, "Linked to NSG %s through subnet %s", *subnet.SubnetPropertiesFormat.NetworkSecurityGroup.Name, *subnet.Name)
}
m.Log.Infof([]string{VMName, *nic.Name, *ipConfig.PrivateIPAddress}, "Subnet %s, in Virtual Network %s", subnetName, VNETName)
vnet, err := getVNET(subscriptionID, subnetRGName, VNETName, "")
if err != nil {
m.Log.Warnf([]string{VMName, *nic.Name, *ipConfig.PrivateIPAddress, *subnet.Name}, "Unable to get Virtual Network %s details", VNETName)
continue
}
m.Log.Infof([]string{VMName, *nic.Name, *ipConfig.PrivateIPAddress}, "Virtual Network %s has %s subnets as potential targets", VNETName, len(*vnet.VirtualNetworkPropertiesFormat.Subnets))
for _, subnet := range *vnet.VirtualNetworkPropertiesFormat.Subnets {
m.Log.Infof([]string{VMName, *nic.Name, *ipConfig.PrivateIPAddress}, "Potential target : %s", *subnet.SubnetPropertiesFormat.AddressPrefix)
targetSubnets = append(targetSubnets, *subnet.SubnetPropertiesFormat.AddressPrefix)
targetSubnetsAlt[*subnet.SubnetPropertiesFormat.AddressPrefix] = []string{}
}
for _, peering := range *vnet.VirtualNetworkPropertiesFormat.VirtualNetworkPeerings {
remoteVnetID := *peering.VirtualNetworkPeeringPropertiesFormat.RemoteVirtualNetwork.ID
remoteVnetResource, _ := azure.ParseResourceID(remoteVnetID)
vmSubscriptionIDs[remoteVnetResource.SubscriptionID] = true
if !*peering.VirtualNetworkPeeringPropertiesFormat.AllowForwardedTraffic {
m.Log.Warnf([]string{VMName, *nic.Name, *ipConfig.PrivateIPAddress, *subnet.Name}, "Peering to Virtual Network %s is disabled", remoteVnetResource.ResourceName)
} else {
m.Log.Successf([]string{VMName, *nic.Name, *ipConfig.PrivateIPAddress, *subnet.Name}, "Peering to Virtual Network %s is active : %s",
remoteVnetResource.ResourceName, stringAndArrayToString(nil, peering.VirtualNetworkPeeringPropertiesFormat.RemoteAddressSpace.AddressPrefixes, " "))
for _, addressPrefix := range *peering.VirtualNetworkPeeringPropertiesFormat.RemoteAddressSpace.AddressPrefixes {
targetSubnets = append(targetSubnets, addressPrefix)
targetSubnetsAlt[addressPrefix] = []string{}
}
}
}
}
}
m.Log.Info([]string{VMName}, "Narrowing down potential target subnets with running compute instances...")
var vms []compute.VirtualMachine
for subscriptionID, _ := range vmSubscriptionIDs {
subscriptionVMs, err := getComputeVMsPerSubscription(subscriptionID)
if err != nil {
m.Log.Warnf([]string{VMName}, "Unable to enumerate VMs in subscription %s", subscriptionID)
continue
}
vms = append(vms, subscriptionVMs...)
}
for i, vm := range vms {
fmt.Printf("Processing machine %d%%\r", 100*i/len(vms))
vmResource, _ := azure.ParseResourceID(*vm.ID)
vmDetails, err := getComputeVmInstanceView(subscriptionID, vmResource.ResourceGroup, ptr.ToString(vm.Name))
if err != nil {
m.Log.Warnf([]string{VMName}, "Unable to fetch VM %s details", ptr.ToString(vm.Name))
continue
}
if vmDetails.VirtualMachineProperties != nil && vmDetails.VirtualMachineProperties.InstanceView != nil {
for _, status := range *vmDetails.VirtualMachineProperties.InstanceView.Statuses {
if *status.Code == "PowerState/running" {
privateIPs, _ := getIPs(subscriptionID, vmResource.ResourceGroup, vm)
for targetSubnet, _ := range targetSubnetsAlt {
_, ipnet, _ := net.ParseCIDR(targetSubnet)
for _, privateIP := range privateIPs {
ip := net.ParseIP(privateIP)
if ipnet.Contains(ip) {
targetSubnetsAlt[targetSubnet] = append(targetSubnetsAlt[targetSubnet], privateIP)
}
}
}
break
}
}
}
}
for targetSubnet, targets := range targetSubnetsAlt {
m.Log.Infof([]string{VMName}, "%s subnet has %s addresses", targetSubnet, len(targets))
}
m.writeScanFile(VMName, targetSubnetsAlt)
} else {
m.Log.Errorf(nil, "Virtual Machine %s has no network interface", VMName)
return
}
}
}
func (m *AzNetScanModule) filterVM(vm compute.VirtualMachine) {
}
func getComputeVMsPerSubscription(subscriptionID string) ([]compute.VirtualMachine, error) {
computeClient := internal.GetVirtualMachinesClient(subscriptionID)
var vms []compute.VirtualMachine
for _, rg := range GetResourceGroups(subscriptionID) {
for page, err := computeClient.List(context.TODO(), ptr.ToString(rg.Name), ""); page.NotDone(); page.Next() {
if err != nil {
return nil, fmt.Errorf("could not enumerate resource group %s. %s", rg, err)
} else {
vms = append(vms, page.Values()...)
}
}
}
return vms, nil
}
func (m *AzNetScanModule) writeScanFile(vm string, targetSubnetsAlt map[string][]string) error {
lootDirectory := filepath.Join(m.AzClient.AzOutputDirectory, "loot")
lootFilePath := filepath.Join(lootDirectory, fmt.Sprintf("scan-from-%s.txt", vm))
err := os.MkdirAll(lootDirectory, os.ModePerm)
if err != nil {
return err
}
file, err := os.Create(lootFilePath)
if err != nil {
return err
}
defer file.Close()
for targetSubnet, targets := range targetSubnetsAlt {
cleanSubnet := strings.Replace(targetSubnet, "/", "_", -1)
_, err := file.WriteString(fmt.Sprintf("nmap -p- --max-retries 1 -T4 -Pn -sV -sC -oA %s -iL %s.lst\n", cleanSubnet, cleanSubnet))
if err != nil {
return err
}
targetsFile, err := os.Create(filepath.Join(lootDirectory, fmt.Sprintf("%s.lst", cleanSubnet)))
if err != nil {
return err
}
defer targetsFile.Close()
for _, target := range targets {
targetsFile.WriteString(fmt.Sprintf("%s\n", target))
}
}
m.Log.Successf(nil, "Scan file written to %s", lootFilePath)
return nil
}
func getSubnetDetails(subscriptionID, resourceGroup, VNETName, subnetName, expand string) (network.Subnet, error) {
client := internal.GetSubnetsClient(subscriptionID)
subnet, err := client.Get(context.TODO(), resourceGroup, VNETName, subnetName, expand)
if err != nil {
return network.Subnet{}, err
}
return subnet, nil
}
func getVNET(subscriptionID, resourceGroup, VNETName, expand string) (network.VirtualNetwork, error) {
client := internal.GetVNETsClient(subscriptionID)
subnet, err := client.Get(context.TODO(), resourceGroup, VNETName, expand)
if err != nil {
return network.VirtualNetwork{}, err
}
return subnet, nil
}
+51
View File
@@ -0,0 +1,51 @@
package azure
import (
"fmt"
"context"
"github.com/BishopFox/cloudfox/internal"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
)
type AzNSGModule struct {
AzClient *internal.AzureClient
getNSGData func(string, *subscriptions.Subscription) error
Log *internal.Logger
}
func (m *AzNSGModule) AzNSGCommand(data string) error {
if data == "links" {
m.AzNSGLinksCommand()
} else if data == "rules" {
m.AzNSGRulesCommand()
} else {
return fmt.Errorf("Invalid data selection for NSG")
}
return nil
}
func (m *AzNSGModule) getNSGInfoPerSubscription(tenantSlug string, AzSubscription *subscriptions.Subscription) error {
var err error
err = m.getNSGData(tenantSlug, AzSubscription)
if err != nil {
return err
}
return nil
}
func (m *AzNSGModule) getNSG(subscriptionID string) (*[]network.SecurityGroup, error) {
nsgClient := internal.GetNSGClient(subscriptionID)
var networkSecurityGroups []network.SecurityGroup
for page, err := nsgClient.ListAll(context.TODO()); page.NotDone(); page.Next() {
if err != nil {
return nil, fmt.Errorf("could not get network security groups for subscription")
}
networkSecurityGroups = append(networkSecurityGroups, page.Values()...)
}
return &networkSecurityGroups, nil
}
+175
View File
@@ -0,0 +1,175 @@
package azure
import (
"context"
"strings"
"fmt"
"path/filepath"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/go-autorest/autorest/azure"
)
func (m *AzNSGModule) AzNSGLinksCommand() error {
m.getNSGData = m.getNSGLinksData
if len(m.AzClient.AzTenants) > 0 {
for _, AzTenant := range m.AzClient.AzTenants {
m.Log.Infof([]string{"links"}, "Enumerating Network Security Group links for tenant %s (%s)", ptr.ToString(AzTenant.DefaultDomain), ptr.ToString(AzTenant.TenantID))
for _, AzTenant := range m.AzClient.AzTenants {
for _, AzSubscription := range GetSubscriptionsPerTenantID(ptr.ToString(AzTenant.TenantID)) {
m.runNSGLinksCommandForSingleSubcription(*AzTenant.DefaultDomain, &AzSubscription)
}
}
}
} else {
for tenantSlug, AzSubscriptions := range m.AzClient.AzSubscriptionsAlt {
for _, AzSubscription := range AzSubscriptions {
m.runNSGLinksCommandForSingleSubcription(tenantSlug, AzSubscription)
}
}
}
return nil
}
func (m *AzNSGModule) runNSGLinksCommandForSingleSubcription(tenantSlug string, AzSubscription *subscriptions.Subscription) error {
var err error
m.Log.Infof([]string{"links"}, "Enumerating Network Security Groups links for subscription %s (%s)", *AzSubscription.DisplayName, *AzSubscription.SubscriptionID)
err = m.getNSGInfoPerSubscription(tenantSlug, AzSubscription)
if err != nil {
return err
}
return nil
}
func (m *AzNSGModule) getNSGLinksData(tenantSlug string, AzSubscription *subscriptions.Subscription) error {
tableHeader := []string{"Network Security Group", "Link Type", "Linked Name", "Link Target"}
var tableBody [][]string
networkSecurityGroups, err := m.getNSG(*AzSubscription.SubscriptionID)
nsgClient := internal.GetNSGClient(*AzSubscription.SubscriptionID)
//subnetsClient := internal.GetSubnetsClient(subscriptionID)
if err != nil {
return err
}
for _, networkSecurityGroup := range *networkSecurityGroups {
tableBody = nil
// setup logging client
o := internal.OutputClient{
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_NSG_LINKS_MODULE_NAME,
Table: internal.TableClient{
Wrap: m.AzClient.AzWrapTable,
},
}
var resource azure.Resource
resource, err = azure.ParseResourceID(*networkSecurityGroup.ID)
if err != nil {
continue
}
networkSecurityGroup, err = nsgClient.Get(context.TODO(), resource.ResourceGroup, resource.ResourceName, "Subnets,NetworkInterfaces")
if err != nil {
m.Log.Warnf([]string{"links"}, "Failed to enumerate links for NSG %s", *networkSecurityGroup.Name)
continue
} else {
m.Log.Infof([]string{"links"}, "Enumerating rules for NSG %s", *networkSecurityGroup.Name)
}
if networkSecurityGroup.Subnets != nil {
for _, subnet := range *networkSecurityGroup.Subnets {
var addressPrefixes []string
if subnet.AddressPrefixes != nil {
for _, prefix := range *subnet.AddressPrefixes {
addressPrefixes = append(addressPrefixes, prefix)
}
}
if subnet.AddressPrefix != nil {
addressPrefixes = append(addressPrefixes, *subnet.AddressPrefix)
}
tableBody = append(tableBody,
[]string{
*networkSecurityGroup.Name,
"Subnet",
ptr.ToString(subnet.Name),
strings.Join(addressPrefixes[:], "\n"),
},
)
}
}
if networkSecurityGroup.NetworkInterfaces != nil {
for _, networkInterface := range *networkSecurityGroup.NetworkInterfaces {
tableBody = append(tableBody,
[]string{
*networkSecurityGroup.Name,
"NIC",
ptr.ToString(networkInterface.Name),
strings.Join(internal.GetIPAddressesFromInterface(&networkInterface)[:], "\n"),
},
)
}
}
// set up table vars
o.PrefixIdentifier = fmt.Sprintf("%s", *AzSubscription.DisplayName)
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantSlug, *AzSubscription.DisplayName)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: tableHeader,
Body: tableBody,
Name: fmt.Sprintf("%s-%s", globals.AZ_NSG_LINKS_MODULE_NAME, *networkSecurityGroup.Name)})
if tableBody != nil {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
}
return nil
}
func stringAndArrayToString(value *string, values *[]string, separator string) string {
var final string
if value != nil {
final = *value
} else {
final = ""
}
if len(*values) > 0 {
if len(final) > 0 {
final = fmt.Sprintf("%s%s%s", final, separator, strings.Join((*values)[:], separator))
} else {
final = strings.Join((*values)[:], separator)
}
}
return final
}
func getSourceFromSecurityGroupRule(rule *network.SecurityRule) string {
var final string
final = stringAndArrayToString(rule.SecurityRulePropertiesFormat.SourceAddressPrefix, rule.SecurityRulePropertiesFormat.SourceAddressPrefixes, "\n")
if rule.SecurityRulePropertiesFormat.SourceApplicationSecurityGroups != nil {
for _, app := range *rule.SecurityRulePropertiesFormat.SourceApplicationSecurityGroups {
resource, _ := azure.ParseResourceID(ptr.ToString(app.ID))
final = fmt.Sprintf("%s\n%s", final, resource.ResourceName)
}
}
return final
}
func getDestinationFromSecurityGroupRule(rule *network.SecurityRule) string {
var final string
final = stringAndArrayToString(rule.SecurityRulePropertiesFormat.DestinationAddressPrefix, rule.SecurityRulePropertiesFormat.DestinationAddressPrefixes, "\n")
if rule.SecurityRulePropertiesFormat.DestinationApplicationSecurityGroups != nil {
for _, app := range *rule.SecurityRulePropertiesFormat.DestinationApplicationSecurityGroups {
resource, _ := azure.ParseResourceID(ptr.ToString(app.ID))
final = fmt.Sprintf("%s\n%s", final, resource.ResourceName)
}
}
return final
}
+125
View File
@@ -0,0 +1,125 @@
package azure
import (
"fmt"
"path/filepath"
"context"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
)
func (m *AzNSGModule) AzNSGRulesCommand() error {
m.getNSGData = m.getNSGRulesData
if len(m.AzClient.AzTenants) > 0 {
for _, AzTenant := range m.AzClient.AzTenants {
m.Log.Infof([]string{"rules"}, "Enumerating Network Security Group rules for tenant %s (%s)", ptr.ToString(AzTenant.DefaultDomain), ptr.ToString(AzTenant.TenantID))
for _, AzTenant := range m.AzClient.AzTenants {
// cloudfox azure nsg-rules --tenant [TENANT_ID | PRIMARY_DOMAIN]
for _, AzSubscription := range GetSubscriptionsPerTenantID(ptr.ToString(AzTenant.TenantID)) {
m.runNSGRulesCommandForSingleSubcription(*AzTenant.DefaultDomain, &AzSubscription)
}
}
}
} else {
for tenantSlug, AzSubscriptions := range m.AzClient.AzSubscriptionsAlt {
for _, AzSubscription := range AzSubscriptions {
m.runNSGRulesCommandForSingleSubcription(tenantSlug, AzSubscription)
}
}
}
return nil
}
func (m *AzNSGModule) runNSGRulesCommandForSingleSubcription(tenantSlug string, AzSubscription *subscriptions.Subscription) error {
var err error
m.Log.Infof([]string{"rules"}, "Enumerating Network Security Groups rules for subscription %s (%s)", *AzSubscription.DisplayName, *AzSubscription.SubscriptionID)
err = m.getNSGInfoPerSubscription(tenantSlug, AzSubscription)
if err != nil {
return err
}
return nil
}
func (m *AzNSGModule) getNSGRulesData(tenantSlug string, AzSubscription *subscriptions.Subscription) error {
tableHeader := []string{"Name", "Rule Type", "Protocol", "Source", "Destination", "Destination port", "Action", "Description"}
var tableBody [][]string
networkSecurityGroups, err := m.getNSG(*AzSubscription.SubscriptionID)
nsgClient := internal.GetNSGClient(*AzSubscription.SubscriptionID)
//subnetsClient := internal.GetSubnetsClient(subscriptionID)
if err != nil {
return err
}
for _, networkSecurityGroup := range *networkSecurityGroups {
tableBody = nil
// setup logging client
o := internal.OutputClient{
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_NSG_RULES_MODULE_NAME,
Table: internal.TableClient{
Wrap: m.AzClient.AzWrapTable,
},
}
var resource azure.Resource
resource, err = azure.ParseResourceID(*networkSecurityGroup.ID)
if err != nil {
continue
}
networkSecurityGroup, err = nsgClient.Get(context.TODO(), resource.ResourceGroup, resource.ResourceName, "")
if err != nil {
m.Log.Warnf([]string{"rules"}, "Failed to enumerate rules for NSG %s", *networkSecurityGroup.Name)
continue
} else {
m.Log.Infof([]string{"rules"}, "Enumerating rules for NSG %s", *networkSecurityGroup.Name)
}
for _, securityRule := range *networkSecurityGroup.SecurityRules {
tableBody = append(tableBody,
[]string{
*securityRule.Name,
fmt.Sprintf("%v", securityRule.SecurityRulePropertiesFormat.Direction),
fmt.Sprintf("%v", securityRule.SecurityRulePropertiesFormat.Protocol),
getSourceFromSecurityGroupRule(&securityRule),
getDestinationFromSecurityGroupRule(&securityRule),
stringAndArrayToString(securityRule.SecurityRulePropertiesFormat.DestinationPortRange,
securityRule.SecurityRulePropertiesFormat.DestinationPortRanges, "\n"),
m.colorRule(fmt.Sprintf("%v", securityRule.SecurityRulePropertiesFormat.Access)),
ptr.ToString(securityRule.Description),
},
)
}
// set up table vars
o.PrefixIdentifier = fmt.Sprintf("%s", *AzSubscription.DisplayName)
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantSlug, *AzSubscription.DisplayName)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: tableHeader,
Body: tableBody,
Name: fmt.Sprintf("%s-%s", globals.AZ_NSG_RULES_MODULE_NAME, *networkSecurityGroup.Name)})
if tableBody != nil {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
}
return nil
}
func (m *AzNSGModule) colorRule(action string) string {
if action == "Allow" {
return m.Log.Green(action)
} else if action == "Deny" {
return m.Log.Red(action)
} else {
return action
}
}
+74 -69
View File
@@ -16,82 +16,80 @@ import (
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
type AzRBACModule struct {
AzClient *internal.AzureClient
Log *internal.Logger
}
func (m *AzRBACModule) AzRBACCommand() error {
// setup logging client
o := internal.OutputClient{
Verbosity: AzVerbosity,
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_RBAC_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
Wrap: m.AzClient.AzWrapTable,
},
}
// initiate command specific client
var c CloudFoxRBACclient
c.RBACModule = m
// set up table vars
var header []string
var body [][]string
var AzSubscriptionInfo SubsriptionInfo
if AzTenantID != "" && AzSubscription == "" {
if len(m.AzClient.AzTenants) > 0 {
// cloudfox azure rbac --tenant [TENANT_ID | PRIMARY_DOMAIN]
for _, AzTenant := range m.AzClient.AzTenants {
var err error
if err != nil {
return err
}
o.PrefixIdentifier = *AzTenant.DefaultDomain
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, *AzTenant.DefaultDomain, "1-tenant-level")
m.Log.Infof(nil, "Enumerating RBAC permissions for tenant %s (%s)", *AzTenant.DefaultDomain, *AzTenant.TenantID)
header, body, err = m.getRBACperTenant(ptr.ToString(AzTenant.TenantID), c)
if err != nil {
return err
}
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_RBAC_MODULE_NAME)})
var err error
tenantInfo := populateTenant(AzTenantID)
if err != nil {
return err
}
o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain)
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level")
fmt.Printf("[%s][%s] Enumerating RBAC permissions for tenant %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME),
fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)))
header, body, err = getRBACperTenant(ptr.ToString(tenantInfo.ID), c)
if err != nil {
return err
}
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_RBAC_MODULE_NAME)})
} else if AzTenantID == "" && AzSubscription != "" {
// cloudfox azure rbac --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME]
tenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription))
tenantInfo := populateTenant(tenantID)
AzSubscriptionInfo = PopulateSubsriptionType(AzSubscription)
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
fmt.Printf("[%s][%s] Enumerating RBAC permissions for subscription %s\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
header, body = getRBACperSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID, c)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_RBAC_MODULE_NAME)})
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
// cloudfox azure rbac --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME]
for _, AzSubscription := range m.AzClient.AzSubscriptions {
tenantInfo := populateTenant(*AzSubscription.TenantID)
o.PrefixIdentifier = ptr.ToString(AzSubscription.DisplayName)
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), *AzSubscription.DisplayName)
m.Log.Infof(nil, "Enumerating RBAC permissions for subscription %s (%s)", ptr.ToString(AzSubscription.DisplayName), *AzSubscription.SubscriptionID)
header, body = m.getRBACperSubscription(ptr.ToString(tenantInfo.ID), *AzSubscription.SubscriptionID, c)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_RBAC_MODULE_NAME)})
}
}
if body != nil {
//internal.OutputSelector(AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_RBAC_MODULE_NAME, AzWrapTable, controlMessagePrefix)
//internal.OutputSelector(m.AzClient.AzVerbosity, AzOutputFormat, header, body, outputDirectory, fileNameWithoutExtension, globals.AZ_RBAC_MODULE_NAME, AzWrapTable, controlMessagePrefix)
o.WriteFullOutput(o.Table.TableFiles, nil)
}
return nil
}
func getRBACperTenant(AzTenantID string, c CloudFoxRBACclient) ([]string, [][]string, error) {
func (m *AzRBACModule) getRBACperTenant(AzTenantID string, c CloudFoxRBACclient) ([]string, [][]string, error) {
var selectedSubs, resultsHeader []string
var resultsBody, b [][]string
for _, s := range GetSubscriptions() {
@@ -110,7 +108,7 @@ func getRBACperTenant(AzTenantID string, c CloudFoxRBACclient) ([]string, [][]st
return resultsHeader, resultsBody, nil
}
func getRBACperSubscription(AzTenantID, AzSubscriptionID string, c CloudFoxRBACclient) ([]string, [][]string) {
func (m *AzRBACModule) getRBACperSubscription(AzTenantID, AzSubscriptionID string, c CloudFoxRBACclient) ([]string, [][]string) {
var resultsHeader []string
var resultsBody [][]string
for _, s := range GetSubscriptions() {
@@ -126,6 +124,7 @@ type CloudFoxRBACclient struct {
roleAssignments []authorization.RoleAssignment
roleDefinitions []authorization.RoleDefinition
AADUsers []graphrbac.User
RBACModule *AzRBACModule
}
func (c *CloudFoxRBACclient) initialize(tenantID string, subscriptionIDs []string) error {
@@ -134,19 +133,19 @@ func (c *CloudFoxRBACclient) initialize(tenantID string, subscriptionIDs []strin
c.roleAssignments = nil
c.roleDefinitions = nil
c.AADUsers, err = getAzureADUsers(tenantID)
c.AADUsers, err = c.RBACModule.getAzureADUsers(tenantID)
if err != nil {
return fmt.Errorf("[%s] failed to get users for tenant %s: %s", color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME), tenantID, err)
}
for _, subID := range subscriptionIDs {
rd, err := getRoleDefinitions(subID)
rd, err := c.RBACModule.getRoleDefinitions(subID)
if err != nil {
fmt.Printf("[%s] failed to get role definitions for subscription %s: %s. Skipping it.\n", color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME), subID, err)
}
c.roleDefinitions = append(c.roleDefinitions, rd...)
ra, err := getRoleAssignments(subID)
ra, err := c.RBACModule.getRoleAssignments(subID)
if err != nil {
fmt.Printf("[%s] failed to get role assignments for subscription %s: %s. Skipping it.\n", color.New(color.FgCyan).Sprint(globals.AZ_RBAC_MODULE_NAME), subID, err)
}
@@ -171,7 +170,7 @@ func (c *CloudFoxRBACclient) GetRelevantRBACData(tenantID, subscriptionID string
}
// Sort the results by userDisplayName using slice.Sort
sortedResults := results
sort.Slice(sortedResults, func(i, j int) bool {
sort.Slice(sortedResults, func (i, j int) bool {
return sortedResults[i].userDisplayName < sortedResults[j].userDisplayName
})
@@ -216,9 +215,11 @@ type RoleAssignmentRelevantData struct {
roleName string
}
var getAzureADUsers = getAzureADUsersOriginal
func (m *AzRBACModule) getAzureADUsers(tenantID string) ([]graphrbac.User, error) {
return m.getAzureADUsersOriginal(tenantID)
}
func getAzureADUsersOriginal(tenantID string) ([]graphrbac.User, error) {
func (m *AzRBACModule) getAzureADUsersOriginal(tenantID string) ([]graphrbac.User, error) {
var users []graphrbac.User
client := internal.GetAADUsersClient(tenantID)
for page, err := client.List(context.TODO(), "", ""); page.NotDone(); page.Next() {
@@ -234,7 +235,7 @@ func getAzureADUsersOriginal(tenantID string) ([]graphrbac.User, error) {
return users, nil
}
func mockedGetAzureADUsers(tenantID string) ([]graphrbac.User, error) {
func (m *AzRBACModule) mockedGetAzureADUsers(tenantID string) ([]graphrbac.User, error) {
var users AzureADUsersTestFile
file, err := os.ReadFile(globals.AAD_USERS_TEST_FILE)
@@ -248,10 +249,10 @@ func mockedGetAzureADUsers(tenantID string) ([]graphrbac.User, error) {
return users.AzureADUsers, nil
}
func generateAzureADUsersTestFIle(tenantID string) {
func (m *AzRBACModule) generateAzureADUsersTestFIle(tenantID string) {
// The READ-ONLY ObjectID attribute needs to be included manually in the test file
// ObjectID *string `json:"objectId,omitempty"`
users, err := getAzureADUsers(tenantID)
users, err := m.getAzureADUsers(tenantID)
if err != nil {
log.Fatalf("could not enumerate users for tenant %s", tenantID)
}
@@ -269,9 +270,11 @@ type AzureADUsersTestFile struct {
AzureADUsers []graphrbac.User `json:"azureADUsers"`
}
var getRoleDefinitions = getRoleDefinitionsOriginal
func (m *AzRBACModule) getRoleDefinitions(subscriptionID string) ([]authorization.RoleDefinition, error) {
return m.getRoleDefinitionsOriginal(subscriptionID)
}
func getRoleDefinitionsOriginal(subscriptionID string) ([]authorization.RoleDefinition, error) {
func (m *AzRBACModule) getRoleDefinitionsOriginal(subscriptionID string) ([]authorization.RoleDefinition, error) {
client := internal.GetRoleDefinitionsClient(subscriptionID)
var roleDefinitions []authorization.RoleDefinition
for page, err := client.List(context.TODO(), "", ""); page.NotDone(); page.Next() {
@@ -287,7 +290,7 @@ func getRoleDefinitionsOriginal(subscriptionID string) ([]authorization.RoleDefi
return roleDefinitions, nil
}
func mockedGetRoleDefinitions(subscriptionID string) ([]authorization.RoleDefinition, error) {
func (m *AzRBACModule) mockedGetRoleDefinitions(subscriptionID string) ([]authorization.RoleDefinition, error) {
var roleDefinitions RoleDefinitionTestFile
file, err := os.ReadFile(globals.ROLE_DEFINITIONS_TEST_FILE)
if err != nil {
@@ -300,16 +303,16 @@ func mockedGetRoleDefinitions(subscriptionID string) ([]authorization.RoleDefini
return roleDefinitions.RoleDefinitions, nil
}
func generateRoleDefinitionsTestFile(subscriptionID string) {
func (m *AzRBACModule) generateRoleDefinitionsTestFile(subscriptionID string) {
// The READ-ONLY ID attribute needs to be included manually in the test file.
// This attribute is the unique identifier for the role.
// ID *string `json:"id,omitempty"`.
roleDefinitions, err := getRoleDefinitions(subscriptionID)
roleDefinitions, err := m.getRoleDefinitions(subscriptionID)
if err != nil {
log.Fatal(err)
}
roleAssignments, err := getRoleAssignments(subscriptionID)
roleAssignments, err := m.getRoleAssignments(subscriptionID)
if err != nil {
log.Fatal(err)
}
@@ -346,9 +349,11 @@ type RoleDefinitionTestFile struct {
RoleDefinitions []authorization.RoleDefinition `json:"roleDefinitions"`
}
var getRoleAssignments = getRoleAssignmentsOriginal
func (m *AzRBACModule) getRoleAssignments(subscriptionID string) ([]authorization.RoleAssignment, error) {
return m.getRoleAssignmentsOriginal(subscriptionID)
}
func getRoleAssignmentsOriginal(subscriptionID string) ([]authorization.RoleAssignment, error) {
func (m *AzRBACModule) getRoleAssignmentsOriginal(subscriptionID string) ([]authorization.RoleAssignment, error) {
var roleAssignments []authorization.RoleAssignment
client := internal.GetRoleAssignmentsClient(subscriptionID)
for page, err := client.List(context.TODO(), ""); page.NotDone(); page.Next() {
@@ -363,7 +368,7 @@ func getRoleAssignmentsOriginal(subscriptionID string) ([]authorization.RoleAssi
return roleAssignments, nil
}
func mockedGetRoleAssignments(subscriptionID string) ([]authorization.RoleAssignment, error) {
func (m *AzRBACModule) mockedGetRoleAssignments(subscriptionID string) ([]authorization.RoleAssignment, error) {
var allRoleAssignments, roleAssignmentsResults []authorization.RoleAssignment
file, err := os.ReadFile(globals.ROLE_ASSIGNMENTS_TEST_FILE)
if err != nil {
@@ -382,8 +387,8 @@ func mockedGetRoleAssignments(subscriptionID string) ([]authorization.RoleAssign
return roleAssignmentsResults, nil
}
func generateRoleAssignmentsTestFile(subscriptionID string) {
ra, err := getRoleAssignments(subscriptionID)
func (m *AzRBACModule) generateRoleAssignmentsTestFile(subscriptionID string) {
ra, err := m.getRoleAssignments(subscriptionID)
if err != nil {
log.Fatalf("could not generate role assignments for subscription %s", subscriptionID)
}
+1 -1
View File
@@ -116,7 +116,7 @@ func GetDefaultDomainFromTenantID(tenantID string) (string, error) {
func populateTenant(tenantID string) TenantInfo {
for _, t := range getTenants() {
for _, t := range GetTenants() {
if ptr.ToString(t.TenantID) == tenantID || ptr.ToString(t.DefaultDomain) == tenantID {
var subscriptions []SubsriptionInfo
for _, s := range GetSubscriptionsPerTenantID(ptr.ToString(t.ID)) {
+95 -85
View File
@@ -9,98 +9,99 @@ import (
"path/filepath"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
// Color functions
var cyan = color.New(color.FgCyan).SprintFunc()
func AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
type AzStorageModule struct {
AzClient *internal.AzureClient
Log *internal.Logger
}
func (m *AzStorageModule) AzStorageCommand() error {
var publicBlobURLs []string
if AzTenantID != "" && AzSubscription == "" {
if len(m.AzClient.AzTenants) > 0 {
// cloudfox azure storage --tenant [TENANT_ID | PRIMARY_DOMAIN]
tenantInfo := populateTenant(AzTenantID)
for _, AzTenant := range m.AzClient.AzTenants {
if AzMergedTable {
if m.AzClient.AzMergedTable {
// set up table vars
var header []string
var body [][]string
// setup logging client
o := internal.OutputClient{
Verbosity: AzVerbosity,
CallingModule: globals.AZ_STORAGE_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
},
}
// set up table vars
var header []string
var body [][]string
// setup logging client
o := internal.OutputClient{
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_STORAGE_MODULE_NAME,
Table: internal.TableClient{
Wrap: m.AzClient.AzWrapTable,
},
}
var err error
var err error
fmt.Printf("[%s][%s] Enumerating storage accounts for tenant %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_RBAC_MODULE_NAME),
fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)))
m.Log.Infof([]string{}, "Enumerating storage accounts for tenant %s (%s)", ptr.ToString(AzTenant.DefaultDomain), ptr.ToString(AzTenant.TenantID))
o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain)
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level")
o.PrefixIdentifier = ptr.ToString(AzTenant.DefaultDomain)
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(AzTenant.DefaultDomain), "1-tenant-level")
header, body, publicBlobURLs, err = getStorageInfoPerTenant(ptr.ToString(tenantInfo.ID))
header, body, publicBlobURLs, err = m.getStorageInfoPerTenant(ptr.ToString(AzTenant.TenantID))
if err != nil {
return err
}
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_STORAGE_MODULE_NAME)})
if body != nil {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
if publicBlobURLs != nil {
err := writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, o.PrefixIdentifier, o.Table.DirectoryName, publicBlobURLs)
if err != nil {
return err
}
}
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_STORAGE_MODULE_NAME)})
} else {
if body != nil {
o.WriteFullOutput(o.Table.TableFiles, nil)
}
if publicBlobURLs != nil {
err := m.writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, o.PrefixIdentifier, o.Table.DirectoryName, publicBlobURLs)
if err != nil {
return err
}
}
for _, s := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) {
runStorageCommandForSingleSubcription(ptr.ToString(s.SubscriptionID), AzOutputDirectory, AzVerbosity, AzWrapTable, Version)
} else {
for _, AzSubscription := range GetSubscriptionsPerTenantID(ptr.ToString(AzTenant.TenantID)) {
m.runStorageCommandForSingleSubscription(*AzTenant.DefaultDomain, &AzSubscription)
}
}
}
} else if AzTenantID == "" && AzSubscription != "" {
//cloudfox azure storage --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME]
runStorageCommandForSingleSubcription(AzSubscription, AzOutputDirectory, AzVerbosity, AzWrapTable, Version)
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
for tenantSlug, AzSubscriptions := range m.AzClient.AzSubscriptionsAlt {
for _, AzSubscription := range AzSubscriptions {
m.runStorageCommandForSingleSubscription(tenantSlug, AzSubscription)
}
}
}
return nil
}
func runStorageCommandForSingleSubcription(AzSubscription string, AzOutputDirectory string, AzVerbosity int, AzWrapTable bool, Version string) error {
func (m *AzStorageModule) runStorageCommandForSingleSubscription(tenantSlug string, AzSubscription *subscriptions.Subscription) error {
var err error
// setup logging client
o := internal.OutputClient{
Verbosity: AzVerbosity,
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_STORAGE_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
Wrap: m.AzClient.AzWrapTable,
},
}
@@ -109,19 +110,11 @@ func runStorageCommandForSingleSubcription(AzSubscription string, AzOutputDirect
var body [][]string
var publicBlobURLs []string
tenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription))
tenantInfo := populateTenant(tenantID)
AzSubscriptionInfo := PopulateSubsriptionType(AzSubscription)
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
o.PrefixIdentifier = *AzSubscription.DisplayName
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantSlug, *AzSubscription.DisplayName)
fmt.Printf(
"[%s][%s] Enumerating storage accounts for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)),
color.CyanString(globals.AZ_STORAGE_MODULE_NAME),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
//AzTenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription))
header, body, publicBlobURLs, err = getStorageInfoPerSubscription(ptr.ToString(tenantInfo.ID), AzSubscriptionInfo.ID)
m.Log.Infof([]string{}, "Enumerating storage accounts for subscription %s (%s)", *AzSubscription.DisplayName, *AzSubscription.SubscriptionID)
header, body, publicBlobURLs, err = m.getStorageInfoPerSubscription(*AzSubscription.TenantID, *AzSubscription.SubscriptionID)
if err != nil {
return err
}
@@ -136,7 +129,7 @@ func runStorageCommandForSingleSubcription(AzSubscription string, AzOutputDirect
}
if publicBlobURLs != nil {
err := writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, o.PrefixIdentifier, o.Table.DirectoryName, publicBlobURLs)
err := m.writeBlobURLslootFile(globals.AZ_STORAGE_MODULE_NAME, o.PrefixIdentifier, o.Table.DirectoryName, publicBlobURLs)
if err != nil {
return err
}
@@ -145,7 +138,7 @@ func runStorageCommandForSingleSubcription(AzSubscription string, AzOutputDirect
}
func writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory string, publicBlobURLs []string) error {
func (m *AzStorageModule) writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory string, publicBlobURLs []string) error {
lootDirectory := filepath.Join(outputDirectory, "loot")
lootFilePath := filepath.Join(lootDirectory, "public-blob-urls.txt")
@@ -171,14 +164,14 @@ func writeBlobURLslootFile(callingModule, controlMessagePrefix, outputDirectory
return nil
}
func getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, []string, error) {
func (m *AzStorageModule) getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, []string, error) {
var err error
var header []string
var body, b [][]string
var publicBlobURLs []string
for _, s := range GetSubscriptionsPerTenantID(AzTenantID) {
header, b, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
header, b, publicBlobURLs, err = m.getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
if err != nil {
return nil, nil, nil, err
} else {
@@ -188,7 +181,7 @@ func getStorageInfoPerTenant(AzTenantID string) ([]string, [][]string, []string,
return header, body, publicBlobURLs, nil
}
func getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]string, [][]string, []string, error) {
func (m *AzStorageModule) getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]string, [][]string, []string, error) {
var err error
var header []string
var body [][]string
@@ -196,7 +189,7 @@ func getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]strin
for _, s := range GetSubscriptions() {
if ptr.ToString(s.SubscriptionID) == AzSubscriptionID {
header, body, publicBlobURLs, err = getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
header, body, publicBlobURLs, err = m.getRelevantStorageAccountData(AzTenantID, ptr.ToString(s.SubscriptionID))
if err != nil {
return nil, nil, nil, err
}
@@ -205,20 +198,34 @@ func getStorageInfoPerSubscription(AzTenantID, AzSubscriptionID string) ([]strin
return header, body, publicBlobURLs, nil
}
func getRelevantStorageAccountData(tenantID, subscriptionID string) ([]string, [][]string, []string, error) {
func (m *AzStorageModule) getRelevantStorageAccountData(tenantID, subscriptionID string) ([]string, [][]string, []string, error) {
tableHeader := []string{"Subscription Name", "Storage Account Name", "Container Name", "Access Status"}
var tableBody [][]string
var publicBlobURLs []string
storageAccounts, err := getStorageAccounts(subscriptionID)
var urls []string
var containers map[string]string
storageAccounts, err := m.getStorageAccounts(subscriptionID)
if err != nil {
return nil, nil, nil, err
}
for _, sa := range storageAccounts {
blobClient, err := internal.GetStorageAccountBlobClient(tenantID, ptr.ToString(sa.Name))
// if resource group were submitted, do filtering
if len(m.AzClient.AzRGs) > 0 {
fmt.Println(*sa.ID)
for _, AzRG := range m.AzClient.AzRGs {
metaResource, _ := azure.ParseResourceID(*sa.ID)
if metaResource.ResourceGroup == *AzRG.Name {
goto ADD_RESOURCE
}
}
goto SKIP_RESOURCE
}
ADD_RESOURCE:
if err != nil {
return nil, nil, nil, err
}
containers, err := getStorageAccountContainers(blobClient)
containers, err = m.getStorageAccountContainers(blobClient)
if err != nil {
// rather than return an error, we'll just add a row to the table highlighting the storage account name and that we couldn't get the containers
@@ -240,20 +247,23 @@ func getRelevantStorageAccountData(tenantID, subscriptionID string) ([]string, [
containerName,
accessType})
}
urls, err := getPublicBlobURLs(blobClient, ptr.ToString(sa.Name), containers)
urls, err = m.getPublicBlobURLs(blobClient, ptr.ToString(sa.Name), containers)
if err == nil {
continue
//return nil, nil, nil, err
}
publicBlobURLs = append(publicBlobURLs, urls...)
SKIP_RESOURCE:
}
return tableHeader, tableBody, publicBlobURLs, nil
}
var getStorageAccounts = getStorageAccountsOriginal
func (m *AzStorageModule) getStorageAccounts(subscriptionID string) ([]storage.Account, error) {
return m.getStorageAccountsOriginal(subscriptionID)
}
func getStorageAccountsOriginal(subscriptionID string) ([]storage.Account, error) {
func (m *AzStorageModule) getStorageAccountsOriginal(subscriptionID string) ([]storage.Account, error) {
storageClient := internal.GetStorageClient(subscriptionID)
var storageAccounts []storage.Account
for page, err := storageClient.List(context.TODO()); page.NotDone(); page.Next() {
@@ -265,7 +275,7 @@ func getStorageAccountsOriginal(subscriptionID string) ([]storage.Account, error
return storageAccounts, nil
}
func mockedGetStorageAccounts(subscriptionID string) ([]storage.Account, error) {
func (m *AzStorageModule) mockedGetStorageAccounts(subscriptionID string) ([]storage.Account, error) {
testFile, err := os.ReadFile(globals.STORAGE_ACCOUNTS_TEST_FILE)
if err != nil {
return nil, fmt.Errorf("could not open storage accounts test file %s", globals.STORAGE_ACCOUNTS_TEST_FILE)
@@ -284,7 +294,7 @@ func mockedGetStorageAccounts(subscriptionID string) ([]storage.Account, error)
return storageAccountsResults, nil
}
func getStorageAccountContainers(client *azblob.Client) (map[string]string, error) {
func (m *AzStorageModule) getStorageAccountContainers(client *azblob.Client) (map[string]string, error) {
containers := make(map[string]string)
pager := client.NewListContainersPager(&azblob.ListContainersOptions{
Include: azblob.ListContainersInclude{Metadata: true, Deleted: true},
@@ -305,11 +315,11 @@ func getStorageAccountContainers(client *azblob.Client) (map[string]string, erro
return containers, nil
}
func getPublicBlobURLs(client *azblob.Client, storageAccountName string, containers map[string]string) ([]string, error) {
func (m *AzStorageModule) getPublicBlobURLs(client *azblob.Client, storageAccountName string, containers map[string]string) ([]string, error) {
var publicBlobURLs []string
for containerName, accessType := range containers {
if accessType == "public" {
url, err := getPublicBlobURLsForContainer(client, storageAccountName, containerName)
url, err := m.getPublicBlobURLsForContainer(client, storageAccountName, containerName)
if err != nil {
//return nil, err
continue
@@ -320,19 +330,19 @@ func getPublicBlobURLs(client *azblob.Client, storageAccountName string, contain
return publicBlobURLs, nil
}
func getPublicBlobURLsForContainer(client *azblob.Client, storageAccountName, containerName string) ([]string, error) {
blobNames, err := getAllBlobsForContainer(client, containerName)
func (m *AzStorageModule) getPublicBlobURLsForContainer(client *azblob.Client, storageAccountName, containerName string) ([]string, error) {
blobNames, err := m.getAllBlobsForContainer(client, containerName)
if err != nil {
return nil, err
}
publicBlobURLs, err := validatePublicBlobURLs(storageAccountName, containerName, blobNames)
publicBlobURLs, err := m.validatePublicBlobURLs(storageAccountName, containerName, blobNames)
if err != nil {
return nil, err
}
return publicBlobURLs, nil
}
func getAllBlobsForContainer(blobClient *azblob.Client, containerName string) ([]string, error) {
func (m *AzStorageModule) getAllBlobsForContainer(blobClient *azblob.Client, containerName string) ([]string, error) {
var blobNames []string
pager := blobClient.NewListBlobsFlatPager(containerName, &azblob.ListBlobsFlatOptions{
@@ -352,7 +362,7 @@ func getAllBlobsForContainer(blobClient *azblob.Client, containerName string) ([
return blobNames, nil
}
func validatePublicBlobURLs(storageAccountName, containerName string, blobNames []string) ([]string, error) {
func (m *AzStorageModule) validatePublicBlobURLs(storageAccountName, containerName string, blobNames []string) ([]string, error) {
var publicBlobURLs []string
if blobNames == nil {
+2 -2
View File
@@ -69,9 +69,9 @@ func TestAzStorageCommand(t *testing.T) {
}
internal.MockFileSystem(true)
// Mocked functions to simulate Azure calls and responses
getTenants = mockedGetTenants
GetTenants = mockedGetTenants
GetSubscriptions = mockedGetSubscriptions
getResourceGroups = mockedGetResourceGroups
GetResourceGroups = mockedGetResourceGroups
getStorageAccounts = mockedGetStorageAccounts
for _, s := range subtests {
+130 -107
View File
@@ -16,106 +16,108 @@ import (
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, Version string, AzVerbosity int, AzWrapTable bool, AzMergedTable bool) error {
type AzVMsModule struct {
AzClient *internal.AzureClient
Log *internal.Logger
}
if AzTenantID != "" && AzSubscription == "" {
// cloudfox azure vms --tenant [TENANT_ID | PRIMARY_DOMAIN]
tenantInfo := populateTenant(AzTenantID)
if AzMergedTable {
// set up table vars
var header []string
var body [][]string
var userData string
func (m *AzVMsModule) AzVMsCommand() error {
o := internal.OutputClient{
Verbosity: AzVerbosity,
CallingModule: globals.AZ_VMS_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
},
}
fmt.Printf("[%s][%s] Enumerating VMs for tenant %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_VMS_MODULE_NAME),
fmt.Sprintf("%s (%s)", ptr.ToString(tenantInfo.DefaultDomain), ptr.ToString(tenantInfo.ID)))
o.PrefixIdentifier = ptr.ToString(tenantInfo.DefaultDomain)
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "1-tenant-level")
// populate the table data
header, body, userData = getVMsPerTenantID(ptr.ToString(tenantInfo.ID))
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_VMS_MODULE_NAME)})
if body != nil {
if userData != "" {
o.Loot.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), "loot")
o.Loot.LootFiles = append(o.Loot.LootFiles,
internal.LootFile{
Contents: userData,
Name: "virtualmachines-user-data"})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
fmt.Println()
} else {
o.WriteFullOutput(o.Table.TableFiles, nil)
fmt.Println()
}
}
} else {
for _, s := range GetSubscriptionsPerTenantID(ptr.ToString(tenantInfo.ID)) {
runVMsCommandForSingleSubscription(ptr.ToString(s.SubscriptionID), AzOutputDirectory, AzVerbosity, AzWrapTable, Version)
}
}
} else if AzTenantID == "" && AzSubscription != "" {
// cloudfox azure vms --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME]
runVMsCommandForSingleSubscription(AzSubscription, AzOutputDirectory, AzVerbosity, AzWrapTable, Version)
} else {
// Error: please make a valid flag selection
fmt.Println("Please enter a valid input with a valid flag. Use --help for info.")
o := internal.OutputClient{
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_VMS_MODULE_NAME,
Table: internal.TableClient{
Wrap: m.AzClient.AzWrapTable,
},
}
if len(m.AzClient.AzTenants) > 0 {
// cloudfox azure inventory --tenant [TENANT_ID | PRIMARY_DOMAIN]
for _, AzTenant := range m.AzClient.AzTenants {
if m.AzClient.AzMergedTable {
// set up table vars
var header []string
var body [][]string
var userData string
o := internal.OutputClient{
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_INVENTORY_MODULE_NAME,
Table: internal.TableClient{
Wrap: m.AzClient.AzWrapTable,
},
}
m.Log.Infof(nil, "Enumerating VMs for tenant %s (%s)", ptr.ToString(AzTenant.DefaultDomain), ptr.ToString(AzTenant.TenantID))
o.PrefixIdentifier = ptr.ToString(AzTenant.DefaultDomain)
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(AzTenant.DefaultDomain), "1-tenant-level")
//populate the table data
header, body, userData = m.getVMsPerTenantID(ptr.ToString(AzTenant.TenantID))
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
Body: body,
Name: fmt.Sprintf(globals.AZ_VMS_MODULE_NAME)})
if body != nil {
if userData != "" {
o.Loot.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(AzTenant.DefaultDomain), "loot")
o.Loot.LootFiles = append(o.Loot.LootFiles,
internal.LootFile{
Contents: userData,
Name: "virtualmachines-user-data"})
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
fmt.Println()
} else {
o.WriteFullOutput(o.Table.TableFiles, nil)
fmt.Println()
}
}
} else {
for _, AzSubscription := range GetSubscriptionsPerTenantID(ptr.ToString(AzTenant.TenantID)) {
m.runVMsCommandForSingleSubscription(*AzTenant.DefaultDomain, &AzSubscription)
}
}
}
} else {
// ./cloudfox azure inventory --subscription [SUBSCRIPTION_ID | SUBSCRIPTION_NAME]
for tenantSlug, AzSubscriptions := range m.AzClient.AzSubscriptionsAlt {
for _, AzSubscription := range AzSubscriptions {
m.runVMsCommandForSingleSubscription(tenantSlug, AzSubscription)
}
}
}
o.WriteFullOutput(o.Table.TableFiles, nil)
return nil
}
func runVMsCommandForSingleSubscription(AzSubscription string, AzOutputDirectory string, AzVerbosity int, AzWrapTable bool, Version string) error {
func (m *AzVMsModule) runVMsCommandForSingleSubscription(tenantSlug string, AzSubscription *subscriptions.Subscription) error {
// set up table vars
var header []string
var body [][]string
var userData string
o := internal.OutputClient{
Verbosity: AzVerbosity,
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_VMS_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
Wrap: m.AzClient.AzWrapTable,
},
}
var AzSubscriptionInfo SubsriptionInfo
tenantID := ptr.ToString(GetTenantIDPerSubscription(AzSubscription))
tenantInfo := populateTenant(tenantID)
AzSubscriptionInfo = PopulateSubsriptionType(AzSubscription)
o.PrefixIdentifier = AzSubscriptionInfo.Name
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name)
o.PrefixIdentifier = *AzSubscription.DisplayName
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantSlug, *AzSubscription.DisplayName)
fmt.Printf("[%s][%s] Enumerating VMs for subscription %s\n",
color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", Version)), color.CyanString(globals.AZ_VMS_MODULE_NAME),
fmt.Sprintf("%s (%s)", AzSubscriptionInfo.Name, AzSubscriptionInfo.ID))
m.Log.Infof(nil, "Enumerating VMs in subscription %s (%s)", *AzSubscription.DisplayName, *AzSubscription.SubscriptionID)
// populate the table data
header, body, userData = getVMsPerSubscriptionID(AzSubscriptionInfo.ID)
header, body, userData = m.getVMsPerSubscriptionID(tenantSlug, AzSubscription)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
@@ -125,7 +127,7 @@ func runVMsCommandForSingleSubscription(AzSubscription string, AzOutputDirectory
if body != nil {
if userData != "" {
o.Loot.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, ptr.ToString(tenantInfo.DefaultDomain), AzSubscriptionInfo.Name, "loot")
o.Loot.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, tenantSlug, *AzSubscription.DisplayName, "loot")
o.Loot.LootFiles = append(o.Loot.LootFiles,
internal.LootFile{
Contents: userData,
@@ -143,59 +145,65 @@ func runVMsCommandForSingleSubscription(AzSubscription string, AzOutputDirectory
return nil
}
func getVMsPerTenantID(AzTenantID string) ([]string, [][]string, string) {
func (m *AzVMsModule) getVMsPerTenantID(AzTenantID string) ([]string, [][]string, string) {
var resultsHeader []string
var resultsBody, b [][]string
var userDataCombined, userData string
var err error
for _, s := range GetSubscriptionsPerTenantID(AzTenantID) {
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
resultsHeader, b, userData, err = getComputeRelevantData(s, rg)
for _, rg := range GetResourceGroups(ptr.ToString(s.SubscriptionID)) {
resultsHeader, b, userData, err = m.getComputeRelevantData(s, rg)
if err != nil {
fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_VMS_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID))
m.Log.Warnf(nil, "Could not enumerate VMs for resource group %s in subscription %s", ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID))
} else {
resultsBody = append(resultsBody, b...)
userDataCombined += userData
}
}
}
return resultsHeader, resultsBody, userDataCombined
}
func getVMsPerSubscriptionID(AzSubscriptionID string) ([]string, [][]string, string) {
func (m *AzVMsModule) getVMsPerSubscriptionID(tenantSlug string, AzSubscription *subscriptions.Subscription) ([]string, [][]string, string) {
var resultsHeader []string
var resultsBody, b [][]string
var userDataCombined, userData string
var err error
for _, s := range GetSubscriptions() {
if ptr.ToString(s.SubscriptionID) == AzSubscriptionID {
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
resultsHeader, b, userData, err = getComputeRelevantData(s, rg)
if err != nil {
fmt.Printf("[%s] Could not enumerate VMs for resource group %s in subscription %s\n", color.CyanString(globals.AZ_VMS_MODULE_NAME), ptr.ToString(rg.Name), ptr.ToString(s.SubscriptionID))
} else {
resultsBody = append(resultsBody, b...)
userDataCombined += userData
}
}
for _, rg := range GetResourceGroups(ptr.ToString(AzSubscription.SubscriptionID)) {
resultsHeader, b, userData, err = m.getComputeRelevantData(*AzSubscription, rg)
if err != nil {
m.Log.Warnf(nil, "Could not enumerate VMs for resource group %s in subscription %s", ptr.ToString(rg.Name), ptr.ToString(AzSubscription.SubscriptionID))
} else {
resultsBody = append(resultsBody, b...)
userDataCombined += userData
}
}
return resultsHeader, resultsBody, userDataCombined
}
func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group) ([]string, [][]string, string, error) {
func (m *AzVMsModule) getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group) ([]string, [][]string, string, error) {
header := []string{"Subscription Name", "VM Name", "VM Location", "Private IPs", "Public IPs", "Admin Username", "Resource Group Name"}
var body [][]string
var userDataString string
// User has requested specific resource groups, filtering
if len(m.AzClient.AzRGs) > 0 {
for _, AzRG := range m.AzClient.AzRGs {
if *rg.Name == *AzRG.Name {
goto ADD_RESOURCE
}
}
return header, body, userDataString, nil
}
ADD_RESOURCE:
subscriptionID := ptr.ToString(sub.SubscriptionID)
subscriptionName := ptr.ToString(sub.DisplayName)
resourceGroupName := ptr.ToString(rg.Name)
vms, err := getComputeVMsPerResourceGroup(subscriptionID, resourceGroupName)
vms, err := m.getComputeVMsPerResourceGroup(subscriptionID, resourceGroupName)
if err != nil {
return nil, nil, "", fmt.Errorf("error fetching vms for resource group %s: %s", resourceGroupName, err)
}
@@ -250,9 +258,12 @@ func getComputeRelevantData(sub subscriptions.Subscription, rg resources.Group)
return header, body, userDataString, nil
}
var getComputeVMsPerResourceGroup = getComputeVMsPerResourceGroupOriginal
func getComputeVMsPerResourceGroupOriginal(subscriptionID string, resourceGroup string) ([]compute.VirtualMachine, error) {
func (m *AzVMsModule) getComputeVMsPerResourceGroup(subscriptionID string, resourceGroup string) ([]compute.VirtualMachine, error) {
return m.getComputeVMsPerResourceGroupOriginal(subscriptionID, resourceGroup)
}
func (m *AzVMsModule) getComputeVMsPerResourceGroupOriginal(subscriptionID string, resourceGroup string) ([]compute.VirtualMachine, error) {
computeClient := internal.GetVirtualMachinesClient(subscriptionID)
var vms []compute.VirtualMachine
@@ -277,8 +288,16 @@ func getComputeVmInfo(subscriptionID string, resourceGroup string, vmName string
}
return vm, nil
}
func getComputeVmInstanceView(subscriptionID string, resourceGroup string, vmName string) (compute.VirtualMachine, error) {
computeClient := internal.GetVirtualMachinesClient(subscriptionID)
vm, err := computeClient.Get(context.Background(), resourceGroup, vmName, compute.InstanceViewTypesInstanceView)
if err != nil {
return compute.VirtualMachine{}, fmt.Errorf("could not get vm %s. %s", vmName, err)
}
return vm, nil
}
func mockedGetComputeVMsPerResourceGroup(subscriptionID, resourceGroup string) ([]compute.VirtualMachine, error) {
func (m *AzVMsModule) mockedGetComputeVMsPerResourceGroup(subscriptionID, resourceGroup string) ([]compute.VirtualMachine, error) {
testFile, err := os.ReadFile(globals.VMS_TEST_FILE)
if err != nil {
return nil, fmt.Errorf("could not read file %s", globals.VMS_TEST_FILE)
@@ -306,7 +325,7 @@ func getIPs(subscriptionID string, resourceGroup string, vm compute.VirtualMachi
if vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces != nil {
for _, nicReference := range *vm.VirtualMachineProperties.NetworkProfile.NetworkInterfaces {
nic, err := getNICdetails(subscriptionID, resourceGroup, nicReference)
nic, err := getNICdetails(subscriptionID, resourceGroup, nicReference, "")
if err != nil {
return []string{err.Error()}, []string{err.Error()}
}
@@ -330,13 +349,15 @@ func getIPs(subscriptionID string, resourceGroup string, vm compute.VirtualMachi
return privateIPs, publicIPs
}
var getNICdetails = getNICdetailsOriginal
func getNICdetails(subscriptionID string, resourceGroup string, nicReference compute.NetworkInterfaceReference, expand string) (network.Interface, error) {
return getNICdetailsOriginal(subscriptionID, resourceGroup, nicReference, expand)
}
func getNICdetailsOriginal(subscriptionID string, resourceGroup string, nicReference compute.NetworkInterfaceReference) (network.Interface, error) {
func getNICdetailsOriginal(subscriptionID string, resourceGroup string, nicReference compute.NetworkInterfaceReference, expand string) (network.Interface, error) {
client := internal.GetNICClient(subscriptionID)
NICName := strings.Split(ptr.ToString(nicReference.ID), "/")[len(strings.Split(ptr.ToString(nicReference.ID), "/"))-1]
nic, err := client.Get(context.TODO(), resourceGroup, NICName, "")
nic, err := client.Get(context.TODO(), resourceGroup, NICName, expand)
if err != nil {
return network.Interface{}, fmt.Errorf("NICnotFound_%s", NICName)
}
@@ -344,7 +365,7 @@ func getNICdetailsOriginal(subscriptionID string, resourceGroup string, nicRefer
return nic, nil
}
func mockedGetNICdetails(subscriptionID, resourceGroup string, nicReference compute.NetworkInterfaceReference) (network.Interface, error) {
func (m *AzVMsModule) mockedGetNICdetails(subscriptionID, resourceGroup string, nicReference compute.NetworkInterfaceReference) (network.Interface, error) {
testFile, err := os.ReadFile(globals.NICS_TEST_FILE)
if err != nil {
return network.Interface{}, fmt.Errorf("NICnotFound_%s", globals.NICS_TEST_FILE)
@@ -364,7 +385,9 @@ func mockedGetNICdetails(subscriptionID, resourceGroup string, nicReference comp
return network.Interface{}, fmt.Errorf("NICnotFound_%s", ptr.ToString(nicReference.ID))
}
var getPublicIP = getPublicIPOriginal
func getPublicIP(subscriptionID string, resourceGroup string, ip network.InterfaceIPConfiguration) (*string, error) {
return getPublicIPOriginal(subscriptionID, resourceGroup, ip)
}
func getPublicIPOriginal(subscriptionID string, resourceGroup string, ip network.InterfaceIPConfiguration) (*string, error) {
client := internal.GetPublicIPClient(subscriptionID)
@@ -380,7 +403,7 @@ func getPublicIPOriginal(subscriptionID string, resourceGroup string, ip network
return publicIPExpanded.PublicIPAddressPropertiesFormat.IPAddress, nil
}
func mockedGetPublicIP(subscriptionID, resourceGroup string, ip network.InterfaceIPConfiguration) (*string, error) {
func (m *AzVMsModule) mockedGetPublicIP(subscriptionID, resourceGroup string, ip network.InterfaceIPConfiguration) (*string, error) {
f, err := os.ReadFile(globals.PUBLIC_IPS_TEST_FILE)
if err != nil {
return nil, fmt.Errorf("IPNotFound_%s", globals.PUBLIC_IPS_TEST_FILE)
+1 -1
View File
@@ -77,7 +77,7 @@ func TestAzVMsCommand(t *testing.T) {
// Mocked functions to simulate Azure calls and responses
GetSubscriptions = mockedGetSubscriptions
getResourceGroups = mockedGetResourceGroups
GetResourceGroups = mockedGetResourceGroups
getComputeVMsPerResourceGroup = mockedGetComputeVMsPerResourceGroup
getNICdetails = mockedGetNICdetails
getPublicIP = mockedGetPublicIP
+19 -15
View File
@@ -15,27 +15,31 @@ import (
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/kyokomi/emoji"
)
func AzWhoamiCommand(AzOutputDirectory, version string, AzWrapTable bool, AzVerbosity int, AzWhoamiListRGsAlso bool) error {
type AzWhoamiModule struct {
AzClient *internal.AzureClient
Log *internal.Logger
}
func (m *AzWhoamiModule) AzWhoamiCommand(AzWhoamiListRGsAlso bool) error {
o := internal.OutputClient{
Verbosity: AzVerbosity,
Verbosity: m.AzClient.AzVerbosity,
CallingModule: globals.AZ_WHOAMI_MODULE_NAME,
Table: internal.TableClient{
Wrap: AzWrapTable,
Wrap: m.AzClient.AzWrapTable,
},
}
fmt.Printf("[%s][%s] Enumerating Azure CLI sessions...\n", color.CyanString(emoji.Sprintf(":fox:cloudfox %s :fox:", version)), color.CyanString(globals.AZ_WHOAMI_MODULE_NAME))
m.Log.Info(nil, "Enumerating Azure CLI sessions...")
var header []string
var body [][]string
o.PrefixIdentifier = "N/A"
if !AzWhoamiListRGsAlso {
// cloudfox azure whoami
header, body = getWhoamiRelevantDataSubsOnly()
o.Table.DirectoryName = filepath.Join(AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "whoami-data")
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "whoami-data")
// append timestamp to filename (time from epoch)
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
@@ -46,7 +50,7 @@ func AzWhoamiCommand(AzOutputDirectory, version string, AzWrapTable bool, AzVerb
} else {
// cloudfox azure whoami --list-rgs
header, body = getWhoamiRelevantDataPerRG()
o.Table.DirectoryName = filepath.Join(ptr.ToString(internal.GetLogDirPath()), globals.AZ_DIR_BASE, "whoami-data")
o.Table.DirectoryName = filepath.Join(m.AzClient.AzOutputDirectory, globals.CLOUDFOX_BASE_DIRECTORY, globals.AZ_DIR_BASE, "whoami-data")
o.Table.TableFiles = append(o.Table.TableFiles,
internal.TableFile{
Header: header,
@@ -64,10 +68,10 @@ func getWhoamiRelevantDataPerRG() ([]string, [][]string) {
tableHead := []string{"Tenant ID", "Tentant Primary Domain", "Subscription ID", "Subscription Name", "RG Name", "Region"}
var tableBody [][]string
for _, t := range getTenants() {
for _, t := range GetTenants() {
for _, s := range GetSubscriptions() {
if ptr.ToString(t.TenantID) == ptr.ToString(s.TenantID) {
for _, rg := range getResourceGroups(ptr.ToString(s.SubscriptionID)) {
for _, rg := range GetResourceGroups(ptr.ToString(s.SubscriptionID)) {
tableBody = append(
tableBody,
[]string{
@@ -90,7 +94,7 @@ func getWhoamiRelevantDataSubsOnly() ([]string, [][]string) {
tableHead := []string{"Tenant ID", "Tenant Primary Domain", "Subscription ID", "Subscription Name"}
var tableBody [][]string
for _, t := range getTenants() {
for _, t := range GetTenants() {
for _, s := range GetSubscriptions() {
if ptr.ToString(t.TenantID) == ptr.ToString(s.TenantID) {
tableBody = append(
@@ -108,9 +112,9 @@ func getWhoamiRelevantDataSubsOnly() ([]string, [][]string) {
return tableHead, tableBody
}
var getTenants = getTenantsOriginal
var GetTenants = GetTenantsOriginal
func getTenantsOriginal() []subscriptions.TenantIDDescription {
func GetTenantsOriginal() []subscriptions.TenantIDDescription {
tenantsClient := internal.GetTenantsClient()
var results []subscriptions.TenantIDDescription
for page, err := tenantsClient.List(context.TODO()); page.NotDone(); err = page.Next() {
@@ -163,9 +167,9 @@ func mockedGetSubscriptions() []subscriptions.Subscription {
return results
}
var getResourceGroups = getResourceGroupsOriginal
var GetResourceGroups = GetResourceGroupsOriginal
func getResourceGroupsOriginal(subscriptionID string) []resources.Group {
func GetResourceGroupsOriginal(subscriptionID string) []resources.Group {
var results []resources.Group
rgClient := internal.GetResourceGroupsClient(subscriptionID)
+2 -2
View File
@@ -14,9 +14,9 @@ func TestAzWhoamiCommand(t *testing.T) {
fmt.Println("[test case] Azure Whoami Command")
// Mocked functions to simulate Azure calls and responses
getTenants = mockedGetTenants
GetTenants = mockedGetTenants
GetSubscriptions = mockedGetSubscriptions
getResourceGroups = mockedGetResourceGroups
GetResourceGroups = mockedGetResourceGroups
// Test case parameters
internal.MockFileSystem(true)
+5 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/codecommit"
"github.com/aws/aws-sdk-go-v2/service/codedeploy"
"github.com/aws/aws-sdk-go-v2/service/datapipeline"
"github.com/aws/aws-sdk-go-v2/service/docdb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ecr"
@@ -155,7 +156,7 @@ var (
DatabasesCommand = &cobra.Command{
Use: "databases",
Aliases: []string{"db", "rds", "redshift", "dbs"},
Short: "Enumerate databases. Get a loot file with connection strings.",
Short: "Enumerate RDS databases. Get a loot file with connection strings.",
Long: "\nUse case examples:\n" +
os.Args[0] + " aws databases --profile readonly_profile",
PreRun: awsPreRun,
@@ -739,6 +740,7 @@ func runDatabasesCommand(cmd *cobra.Command, args []string) {
RDSClient: rds.NewFromConfig(AWSConfig),
RedshiftClient: redshift.NewFromConfig(AWSConfig),
DynamoDBClient: dynamodb.NewFromConfig(AWSConfig),
DocDBClient: docdb.NewFromConfig(AWSConfig),
Caller: *caller,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
AWSProfile: profile,
@@ -1394,6 +1396,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
codeCommitClient := codecommit.NewFromConfig(AWSConfig)
codeDeployClient := codedeploy.NewFromConfig(AWSConfig)
dataPipelineClient := datapipeline.NewFromConfig(AWSConfig)
docdbClient := docdb.NewFromConfig(AWSConfig)
dynamodbClient := dynamodb.NewFromConfig(AWSConfig)
ec2Client := ec2.NewFromConfig(AWSConfig)
ecrClient := ecr.NewFromConfig(AWSConfig)
@@ -1602,6 +1605,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) {
RDSClient: rdsClient,
RedshiftClient: redshiftClient,
DynamoDBClient: dynamodbClient,
DocDBClient: docdbClient,
Caller: *caller,
AWSProfile: profile,
AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken),
+203 -45
View File
@@ -1,27 +1,46 @@
package cli
import (
"log"
"github.com/BishopFox/cloudfox/azure"
"github.com/BishopFox/cloudfox/internal"
az "github.com/Azure/go-autorest/autorest/azure"
"github.com/spf13/cobra"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/subscriptions"
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
)
var (
AzTenantID string
AzSubscription string
AzRGName string
AzOutputFormat string
AzOutputDirectory string
AzVerbosity int
AzWrapTable bool
AzMergedTable bool
// output options
AzOutputFormat string
AzOutputDirectory string
AzVerbosity int
AzWrapTable bool
AzMergedTable bool
// resource selectors
AzTenantRefs []string
AzSubscriptionRefs []string
AzRGRefs []string
AzResourceRefs []string
// Shared resources for all modules
AzClient *internal.AzureClient
AzTenants []*subscriptions.TenantIDDescription
AzSubscriptions []*subscriptions.Subscription
AzRGs []*resources.Group
AzResources []*az.Resource
// command specific variables
// netscan ipv4
AzSourceIPv4 string
AzCommands = &cobra.Command{
Use: "azure",
Aliases: []string{"az"},
Long: `See "Available Commands" for Azure Modules below`,
Short: "See \"Available Commands\" for Azure Modules below",
PersistentPreRun: azurePreRun,
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
@@ -34,12 +53,8 @@ var (
Long: `
Display Available Azure CLI Sessions:
./cloudfox az whoami`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzWhoamiCommand(AzOutputDirectory, cmd.Root().Version, AzWrapTable, AzVerbosity, AzWhoamiListRGsAlso)
if err != nil {
log.Fatal(err)
}
},
Run: runAzWhoamiCommand,
PersistentPreRun: func (cmd *cobra.Command, args []string) {},
}
AzInventoryCommand = &cobra.Command{
Use: "inventory",
@@ -52,12 +67,7 @@ Enumerate inventory for a specific tenant:
Enumerate inventory for a specific subscription:
./cloudfox az inventory --subscription SUBSCRIPTION_ID
`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzInventoryCommand(AzTenantID, AzSubscription, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
}
},
Run: runAzInventoryCommand,
}
AzRBACCommand = &cobra.Command{
Use: "rbac",
@@ -70,13 +80,7 @@ Enumerate role assignments for a specific tenant:
Enumerate role assignments for a specific subscription:
./cloudfox az rbac --subscription SUBSCRIPTION_ID
`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzRBACCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
}
},
Run: runAzRBACCommand,
}
AzVMsCommand = &cobra.Command{
Use: "vms",
@@ -88,12 +92,7 @@ Enumerate VMs for a specific tenant:
Enumerate VMs for a specific subscription:
./cloudfox az vms --subscription SUBSCRIPTION_ID`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzVMsCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
}
},
Run: runAzVMsCommand,
}
AzStorageCommand = &cobra.Command{
Use: "storage",
@@ -106,26 +105,182 @@ Enumerate storage accounts for a specific tenant:
Enumerate storage accounts for a specific subscription:
./cloudfox az storage --subscription SUBSCRIPTION_ID
`,
Run: func(cmd *cobra.Command, args []string) {
err := azure.AzStorageCommand(AzTenantID, AzSubscription, AzOutputFormat, AzOutputDirectory, cmd.Root().Version, AzVerbosity, AzWrapTable, AzMergedTable)
if err != nil {
log.Fatal(err)
}
},
Run: runAzStorageCommand,
}
AzNSGRulesCommand = &cobra.Command{
Use: "nsg-rules",
Aliases: []string{},
Short: "Enumerates azure Network Security Group rules",
Long: `
Enumerate Network Security Groups rules for a specific tenant:
./cloudfox az nsg-rukes --tenant TENANT_ID
Enumerate Network Security Groups rules for a specific subscription:
./cloudfox az nsg-rules --subscription SUBSCRIPTION_ID
Enumerate rules for a specific Network Security Group:
./cloudfox az nsg-rules --nsg NSG_ID
`,
Run: runAzNSGRulesCommand,
}
AzNSGLinksCommand = &cobra.Command{
Use: "nsg-links",
Aliases: []string{},
Short: "Enumerates azure Network Security Groups links",
Long: `
Enumerate Network Security Groups links for a specific tenant:
./cloudfox az nsg-links --tenant TENANT_ID
Enumerate Network Security Groups links for a specific subscription:
./cloudfox az nsg-links --subscription SUBSCRIPTION_ID
Enumerate links for a specific Network Security Group:
./cloudfox az nsg-links --nsg NSG_ID
`,
Run: runAzNSGLinksCommand,
}
AzNetScanCommand = &cobra.Command{
Use: "netscan",
Aliases: []string{},
Short: "Enumerates internal network targets",
Long: `
Enumerate targets with a specific resource as origin:
./cloudfox az nsg-links --resource-id /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
Enumerate targets with a specific IP address as origin
./cloudfox az nsg-links --ipv4 A.B.C.D
`,
Run: runAzNetScanCommand,
}
)
func runAzWhoamiCommand (cmd *cobra.Command, args []string) {
AzClient = new(internal.AzureClient)
AzClient.Log = internal.NewLogger("azure")
AzClient.Log.Announce(nil, "Analyzing local Azure credentials")
AzClient.Version = cmd.Root().Version
AzClient.AzWrapTable = AzWrapTable
AzClient.AzMergedTable = AzMergedTable
AzClient.AzVerbosity = AzVerbosity
AzClient.AzOutputFormat = AzOutputFormat
AzClient.AzOutputDirectory = AzOutputDirectory
m := azure.AzWhoamiModule{
AzClient: AzClient,
Log: internal.NewLogger("whoami"),
}
err := m.AzWhoamiCommand(AzWhoamiListRGsAlso)
if err != nil {
m.Log.Fatal(nil, err.Error())
}
}
func runAzInventoryCommand (cmd *cobra.Command, args []string) {
m := azure.AzInventoryModule{
AzClient: AzClient,
Log: internal.NewLogger("inventory"),
}
err := m.AzInventoryCommand()
if err != nil {
m.Log.Fatal(nil, err.Error())
}
}
func runAzRBACCommand (cmd *cobra.Command, args []string) {
m := azure.AzRBACModule{
AzClient: AzClient,
Log: internal.NewLogger("rbac"),
}
err := m.AzRBACCommand()
if err != nil {
m.Log.Fatal(nil, err.Error())
}
}
func runAzVMsCommand (cmd *cobra.Command, args []string) {
m := azure.AzVMsModule{
AzClient: AzClient,
Log: internal.NewLogger("vms"),
}
err := m.AzVMsCommand()
if err != nil {
m.Log.Fatal(nil, err.Error())
}
}
func runAzStorageCommand (cmd *cobra.Command, args []string) {
m := azure.AzStorageModule{
AzClient: AzClient,
Log: internal.NewLogger("storage"),
}
err := m.AzStorageCommand()
if err != nil {
m.Log.Fatal(nil, err.Error())
}
}
func runAzNSGRulesCommand(cmd *cobra.Command, args []string) {
m := azure.AzNSGModule{
AzClient: AzClient,
Log: internal.NewLogger("nsg"),
}
err := m.AzNSGCommand("rules")
if err != nil {
m.Log.Fatal([]string{"rules"}, err.Error())
}
}
func runAzNSGLinksCommand(cmd *cobra.Command, args []string) {
m := azure.AzNSGModule{
AzClient: AzClient,
Log: internal.NewLogger("nsg"),
}
err := m.AzNSGCommand("links")
if err != nil {
m.Log.Fatal([]string{"links"}, err.Error())
}
}
func runAzNetScanCommand(cmd *cobra.Command, args []string) {
m := azure.AzNetScanModule{
AzClient: AzClient,
Log: internal.NewLogger("netscan"),
}
err := m.AzNetScanCommand(AzSourceIPv4)
if err != nil {
m.Log.Fatal(nil, err.Error())
}
}
func azurePreRun(cmd *cobra.Command, args []string) {
AzClient = internal.NewAzureClient(AzVerbosity, AzWrapTable, AzMergedTable, AzTenantRefs, AzSubscriptionRefs, AzRGRefs, AzResourceRefs, cmd, AzOutputFormat, AzOutputDirectory)
nTenants := len(AzClient.AzTenants)
nSubscriptions := len(AzClient.AzSubscriptions)
nRGs := len(AzClient.AzRGs)
nResources := len(AzClient.AzResources)
nTotal := nTenants + nSubscriptions + nRGs + nResources
if nTotal == 0 {
AzClient.Log.Fatal(nil, "No valid target supplied, stopping")
}
}
func init() {
AzWhoamiCommand.Flags().BoolVarP(&AzWhoamiListRGsAlso, "list-rgs", "l", false, "Drill down to the resource group level")
AzNetScanCommand.Flags().StringVar(&AzSourceIPv4, "ipv4", "", "The Source IP address to run the analysis from")
// Global flags
AzCommands.PersistentFlags().StringVarP(&AzOutputFormat, "output", "o", "all", "[\"table\" | \"csv\" | \"all\" ]")
AzCommands.PersistentFlags().StringVar(&AzOutputDirectory, "outdir", defaultOutputDir, "Output Directory ")
AzCommands.PersistentFlags().IntVarP(&AzVerbosity, "verbosity", "v", 2, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n")
AzCommands.PersistentFlags().StringVarP(&AzTenantID, "tenant", "t", "", "Tenant name")
AzCommands.PersistentFlags().StringVarP(&AzSubscription, "subscription", "s", "", "Subscription ID or Name")
AzCommands.PersistentFlags().StringVarP(&AzRGName, "resource-group", "g", "", "Resource Group name")
AzCommands.PersistentFlags().StringSliceVarP(&AzTenantRefs, "tenant", "t", []string{}, "Tenant ID or name, repeatable")
AzCommands.PersistentFlags().StringSliceVarP(&AzSubscriptionRefs, "subscription", "s", []string{}, "Subscription ID or name, repeatable")
AzCommands.PersistentFlags().StringSliceVarP(&AzRGRefs, "resource-group", "r", []string{}, "Resource Group name or ID, repeatable")
AzCommands.PersistentFlags().StringSliceVarP(&AzResourceRefs, "resource-id", "i", []string{}, "Resource ID (/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}), repeatable")
AzCommands.PersistentFlags().BoolVarP(&AzWrapTable, "wrap", "w", false, "Wrap table to fit in terminal (complicates grepping)")
AzCommands.PersistentFlags().BoolVarP(&AzMergedTable, "merged-table", "m", false, "Writes a single table for all subscriptions in the tenant. Default writes a table per subscription.")
@@ -134,6 +289,9 @@ func init() {
AzRBACCommand,
AzVMsCommand,
AzStorageCommand,
AzInventoryCommand)
AzNSGRulesCommand,
AzNSGLinksCommand,
AzInventoryCommand,
AzNetScanCommand)
}
-109
View File
@@ -1,109 +0,0 @@
package cli
import (
"context"
"fmt"
"github.com/BishopFox/cloudfox/gcp/commands"
oauthservice "github.com/BishopFox/cloudfox/gcp/services/oauthService"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var (
// GCP resources filtering options
GCPOrganization string
GCPProjectID string
GCPProjectIDsFilePath string
GCPProjectIDs []string
// Output formatting options
GCPOutputFormat string
GCPOutputDirectory string
GCPVerbosity int
GCPWrapTable bool
// misc options
// GCPIgnoreCache bool
// logger
GCPLogger = internal.NewLogger()
// GCP root command
GCPCommands = &cobra.Command{
Use: "gcp",
Aliases: []string{"gcloud"},
Long: `See "Available Commands" for GCP Modules below`,
Short: "See \"Available Commands\" for GCP Modules below",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if GCPProjectID != "" {
GCPProjectIDs = append(GCPProjectIDs, GCPProjectID)
} else if GCPProjectIDsFilePath != "" {
GCPProjectIDs = internal.LoadFileLinesIntoArray(GCPProjectIDsFilePath)
} else {
GCPLogger.InfoM("project or project-list flags not given, commands requiring a project ID will fail", "gcp")
}
// Create a context with this value to share it with subcommands at runtime
ctx := context.WithValue(context.Background(), "projectIDs", GCPProjectIDs)
// Set the context for this command which all subcommands can access via [SUBCMD].Parent().Context()
// cmd.SetContext(ctx)
os := oauthservice.NewOAuthService()
principal, err := os.WhoAmI()
if err != nil {
GCPLogger.FatalM(fmt.Sprintf("could not determine default user credential with error %s. \n\nPlease use default application default credentials: https://cloud.google.com/docs/authentication/application-default-credentials", err.Error()), "gcp")
}
ctx = context.WithValue(ctx, "account", principal.Email)
cmd.SetContext(ctx)
},
}
)
// New RunAllGCPCommands function to execute all child commands
var GCPAllChecksCommand = &cobra.Command{
Use: "all-checks",
Short: "Runs all available GCP commands",
Long: `Executes all available GCP commands to collect and display information from all supported GCP services.`,
Run: func(cmd *cobra.Command, args []string) {
for _, childCmd := range GCPCommands.Commands() {
if childCmd == cmd { // Skip the run-all command itself to avoid infinite recursion
continue
}
GCPLogger.InfoM(fmt.Sprintf("Running command: %s", childCmd.Use), "all-checks")
childCmd.Run(cmd, args)
}
},
}
func init() {
// Globals flags for the GCP commands
// Allow selection of non-default account to be used when accessing gcloud API
// TODO
// Resource filtering options
// GCPCommands.PersistentFlags().StringVarP(&GCPOrganization, "organization", "o", "", "Organization name or number, repetable")
GCPCommands.PersistentFlags().StringVarP(&GCPProjectID, "project", "p", "", "GCP project ID")
GCPCommands.PersistentFlags().StringVarP(&GCPProjectIDsFilePath, "project-list", "l", "", "Path to a file containing a list of project IDs separated by newlines")
// GCPCommands.PersistentFlags().BoolVarP(&GCPAllProjects, "all-projects", "a", false, "Use all project IDs available to activated gloud account or given gcloud account")
// GCPCommands.PersistentFlags().BoolVarP(&GCPConfirm, "yes", "y", false, "Non-interactive mode (like apt/yum)")
// GCPCommands.PersistentFlags().StringVarP(&GCPOutputFormat, "output", "", "brief", "[\"brief\" | \"wide\" ]")
GCPCommands.PersistentFlags().IntVarP(&Verbosity, "verbosity", "v", 1, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n")
// defaultOutputDir is defined in cli.aws
GCPCommands.PersistentFlags().StringVar(&GCPOutputDirectory, "outdir", defaultOutputDir, "Output Directory ")
// GCPCommands.PersistentFlags().IntVarP(&Goroutines, "max-goroutines", "g", 30, "Maximum number of concurrent goroutines")
GCPCommands.PersistentFlags().BoolVarP(&GCPWrapTable, "wrap", "w", false, "Wrap table to fit in terminal (complicates grepping)")
// Available commands
GCPCommands.AddCommand(
commands.GCPBucketsCommand,
commands.GCPArtifactRegistryCommand,
commands.GCPBigQueryCommand,
commands.GCPSecretsCommand,
commands.GCPIAMCommand,
commands.GCPInstancesCommand,
commands.GCPWhoAmICommand,
GCPAllChecksCommand,
)
}
-172
View File
@@ -1,172 +0,0 @@
package commands
import (
"fmt"
artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
ArtifactRegistryService "github.com/BishopFox/cloudfox/gcp/services/artifactRegistryService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPArtifactRegistryCommand = &cobra.Command{
Use: globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP artifact registry information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available artifact registry resource information:
cloudfox gcp artfact-registry`,
Run: runGCPArtifactRegistryCommand,
}
// Code needed to output fields from buckets results using generic HandleOutput function
// Results struct that implements the internal.OutputInterface
type GCPArtifactRegistryResults struct {
ArtifactData []ArtifactRegistryService.ArtifactInfo
RepositoryData []ArtifactRegistryService.RepositoryInfo
}
// Decide what format the name, header and body of the CSV & JSON files will be
func (g GCPArtifactRegistryResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
repoHeader := []string{
"Name",
"Format",
"Description",
"Size",
"Location",
"ProjectID",
}
var repoBody [][]string
for _, value := range g.RepositoryData {
repoBody = append(
repoBody,
[]string{
value.Name,
value.Format,
value.Description,
value.SizeBytes,
value.Location,
value.ProjectID,
},
)
}
repoTableFile := internal.TableFile{
Header: repoHeader,
Body: repoBody,
Name: fmt.Sprintf("%s-repos", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
}
tableFiles = append(tableFiles, repoTableFile)
artifactHeader := []string{
"Name",
"Format",
"Version",
"Location",
"Repository",
"Size",
"Updated",
"ProjectID",
}
var artifactBody [][]string
for _, value := range g.ArtifactData {
artifactBody = append(
artifactBody,
[]string{
value.Name,
value.Format,
value.Version,
value.Location,
value.Repository,
value.SizeBytes,
value.Updated,
value.ProjectID,
},
)
}
artifactTableFile := internal.TableFile{
Header: artifactHeader,
Body: artifactBody,
Name: fmt.Sprintf("%s-artifacts", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME),
}
tableFiles = append(tableFiles, artifactTableFile)
return tableFiles
}
// Decide what is loot based on resource information
func (g GCPArtifactRegistryResults) LootFiles() []internal.LootFile {
// TODO consider a loot file of the URLs to the all docker image artifacts. Maybe sample commands to pull the images
return []internal.LootFile{}
}
// Houses high-level logic that retrieves resources and writes to output
func runGCPArtifactRegistryCommand(cmd *cobra.Command, args []string) {
// Retrieve projectIDs from parent (gcp command) ctx
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
client, err := artifactregistry.NewClient(ctx)
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to create secret manager client: %v", err), globals.GCP_SECRETS_MODULE_NAME)
return
}
defer client.Close()
// Get the artifact repositories and artifacts using the projectIDs and ArtifactRegistryService
ars := ArtifactRegistryService.New(client)
var artifactResults []ArtifactRegistryService.ArtifactInfo
var repoRestuls []ArtifactRegistryService.RepositoryInfo
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all artifact repositories and supported artifacts in all locations from project: %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
result, err := ars.RepositoriesAndArtifacts(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return
}
artifactResults = append(artifactResults, result.Artifacts...)
repoRestuls = append(repoRestuls, result.Repositories...)
logger.InfoM(fmt.Sprintf("Done retrieving artifact repository resource data from project: %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
cloudfoxOutput := GCPArtifactRegistryResults{ArtifactData: artifactResults, RepositoryData: repoRestuls}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
}
}
-134
View File
@@ -1,134 +0,0 @@
package commands
import (
"fmt"
"time"
BigQueryService "github.com/BishopFox/cloudfox/gcp/services/bigqueryService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPBigQueryCommand = &cobra.Command{
Use: "bigquery",
Aliases: []string{},
Short: "Display Bigauery datasets and tables information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available Bigauery datasets and tables resource information:
cloudfox gcp bigquery`,
Run: runGCPBigQueryCommand,
}
// GCPBigQueryResults struct that implements the internal.OutputInterface
type GCPBigQueryResults struct {
DatasetsData []BigQueryService.BigqueryDataset
TablesData []BigQueryService.BigqueryTable
}
// Define the format for CSV & JSON output
func (g GCPBigQueryResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
// For Datasets
datasetHeader := []string{"Name", "DatasetID", "Description", "CreationTime", "LastModifiedTime", "Location", "ProjectID"}
var datasetBody [][]string
for _, dataset := range g.DatasetsData {
datasetBody = append(datasetBody, []string{
dataset.Name,
dataset.DatasetID,
dataset.Description,
dataset.CreationTime.Format(time.RFC3339),
dataset.LastModifiedTime.Format(time.RFC3339),
dataset.Location,
dataset.ProjectID,
})
}
datasetTableFile := internal.TableFile{
Header: datasetHeader,
Body: datasetBody,
Name: "bigquery-datasets",
}
tableFiles = append(tableFiles, datasetTableFile)
// For Tables
tableHeader := []string{"TableID", "DatasetID", "Description", "CreationTime", "LastModifiedTime", "NumBytes", "Location", "ProjectID"}
var tableBody [][]string
for _, table := range g.TablesData {
tableBody = append(tableBody, []string{
table.TableID,
table.DatasetID,
table.Description,
table.CreationTime.Format(time.RFC3339),
table.LastModifiedTime.Format(time.RFC3339),
fmt.Sprintf("%d", table.NumBytes),
table.Location,
table.ProjectID,
})
}
tableTableFile := internal.TableFile{
Header: tableHeader,
Body: tableBody,
Name: "bigquery-tables",
}
tableFiles = append(tableFiles, tableTableFile)
return tableFiles
}
func (g GCPBigQueryResults) LootFiles() []internal.LootFile {
// Implement if there's specific data considered as loot
return []internal.LootFile{}
}
func runGCPBigQueryCommand(cmd *cobra.Command, args []string) {
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_BIGQUERY_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_BIGQUERY_MODULE_NAME)
}
bqService := BigQueryService.New()
var datasetsResults []BigQueryService.BigqueryDataset
var tablesResults []BigQueryService.BigqueryTable
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving BigQuery datasets and tables from project: %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
result, err := bqService.BigqueryDatasetsAndTables(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BIGQUERY_MODULE_NAME)
return
}
datasetsResults = append(datasetsResults, result.Datasets...)
tablesResults = append(tablesResults, result.Tables...)
cloudfoxOutput := GCPBigQueryResults{DatasetsData: datasetsResults, TablesData: tablesResults}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_BIGQUERY_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BIGQUERY_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_BIGQUERY_MODULE_NAME)
}
}
-115
View File
@@ -1,115 +0,0 @@
package commands
import (
"fmt"
CloudStorageService "github.com/BishopFox/cloudfox/gcp/services/cloudStorageService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPBucketsCommand = &cobra.Command{
Use: globals.GCP_BUCKETS_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP buckets information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available bucket information:
cloudfox gcp buckets`,
Run: runGCPBucketsCommand,
}
// Code needed to output fields from buckets results using generic HandleOutput function
// Results struct that implements the internal.OutputInterface
type GCPBucketsResults struct {
Data []CloudStorageService.BucketInfo
}
// Decide what format the name, header and body of the CSV & JSON files will be
func (g GCPBucketsResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
header := []string{
"Name",
"Location",
"ProjectID",
}
var body [][]string
for _, value := range g.Data {
body = append(
body,
[]string{
value.Name,
value.Location,
value.ProjectID,
},
)
}
tableFile := internal.TableFile{
Header: header,
Body: body,
Name: globals.GCP_BUCKETS_MODULE_NAME,
}
tableFiles = append(tableFiles, tableFile)
return tableFiles
}
// Decide what is loot based on resource information
func (g GCPBucketsResults) LootFiles() []internal.LootFile {
return []internal.LootFile{}
}
// Houses high-level logic that retrieves resources and writes to output
func runGCPBucketsCommand(cmd *cobra.Command, args []string) {
// Retrieve projectIDs from parent (gcp command) ctx
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_BUCKETS_MODULE_NAME)
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_BUCKETS_MODULE_NAME)
}
// Get the bucket info using the projectIDs and CloudStorageService
cs := CloudStorageService.New()
var results []CloudStorageService.BucketInfo
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all buckets from project: %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
result, err := cs.Buckets(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BUCKETS_MODULE_NAME)
return
}
results = append(results, result...)
logger.InfoM(fmt.Sprintf("Done retrieving all buckets from project: %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
cloudfoxOutput := GCPBucketsResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_BUCKETS_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_BUCKETS_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_BUCKETS_MODULE_NAME)
}
}
-122
View File
@@ -1,122 +0,0 @@
package commands
import (
"fmt"
IAMService "github.com/BishopFox/cloudfox/gcp/services/iamService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPIAMCommand = &cobra.Command{
Use: globals.GCP_IAM_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP IAM information",
Args: cobra.MinimumNArgs(0),
Long: `
Display IAM principals and their roles information within GCP resources:
cloudfox gcp iam`,
Run: runGCPIAMCommand,
}
// Results struct for IAM command that implements the internal.OutputInterface
type GCPIAMResults struct {
Data []IAMService.PrincipalWithRoles
}
// TableFiles formats the data for table output, CSV & JSON files
func (g GCPIAMResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
header := []string{
"Name",
"Principal Type",
"Role",
"PolicyResourceType",
"PolicyResourceID",
}
var body [][]string
for _, principal := range g.Data {
for _, binding := range principal.PolicyBindings {
body = append(body, []string{
principal.Name,
principal.Type,
binding.Role,
principal.ResourceType,
principal.ResourceID,
})
}
}
tableFile := internal.TableFile{
Header: header,
Body: body,
Name: globals.GCP_IAM_MODULE_NAME,
}
tableFiles = append(tableFiles, tableFile)
return tableFiles
}
// LootFiles can be implemented if needed
func (g GCPIAMResults) LootFiles() []internal.LootFile {
return []internal.LootFile{}
}
// Houses high-level logic that retrieves IAM information and writes to output
func runGCPIAMCommand(cmd *cobra.Command, args []string) {
// Retrieve projectIDs and resource type from parent (gcp command) ctx
var projectIDs []string
var resourceType string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_IAM_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_IAM_MODULE_NAME)
}
// TODO fix once folders or organizations are supported as input for project root
resourceType = "project"
// Initialize IAMService and fetch principals with roles for the given projectIDs and resource type
iamService := IAMService.New()
var results []IAMService.PrincipalWithRoles
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving IAM information for resource: %s of type %s", projectID, resourceType), globals.GCP_IAM_MODULE_NAME)
principals, err := iamService.PrincipalsWithRoles(projectID, resourceType)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_IAM_MODULE_NAME)
return
}
results = append(results, principals...)
logger.InfoM(fmt.Sprintf("Done retrieving IAM information for resource: %s of type %s", projectID, resourceType), globals.GCP_IAM_MODULE_NAME)
cloudfoxOutput := GCPIAMResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_IAM_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_IAM_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_IAM_MODULE_NAME)
}
}
-126
View File
@@ -1,126 +0,0 @@
package commands
import (
"fmt"
ComputeEngineService "github.com/BishopFox/cloudfox/gcp/services/computeEngineService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPInstancesCommand = &cobra.Command{
Use: globals.GCP_INSTANCES_MODULE_NAME, // This should be defined in the globals package
Aliases: []string{},
Short: "Display GCP Compute Engine instances information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available Compute Engine instances information:
cloudfox gcp instances`,
Run: runGCPInstancesCommand,
}
// GCPInstancesResults implements internal.OutputInterface for Compute Engine instances
type GCPInstancesResults struct {
Data []ComputeEngineService.ComputeEngineInfo
}
func (g GCPInstancesResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
header := []string{
"Name",
"ID",
"State",
"ExternalIP",
"InternalIP",
"ServiceAccount", // Adding ServiceAccount to the header
"Zone",
"ProjectID",
}
var body [][]string
for _, instance := range g.Data {
// Initialize an empty string to aggregate service account emails
var serviceAccountEmails string
for _, serviceAccount := range instance.ServiceAccounts {
// Assuming each instance can have multiple service accounts, concatenate their emails
if serviceAccountEmails != "" {
serviceAccountEmails += "; " // Use semicolon as a delimiter for multiple emails
}
serviceAccountEmails += serviceAccount.Email
}
body = append(body, []string{
instance.Name,
instance.ID,
instance.State,
instance.ExternalIP,
instance.InternalIP,
serviceAccountEmails, // Add the aggregated service account emails to the output
instance.Zone,
instance.ProjectID,
})
}
tableFiles = append(tableFiles, internal.TableFile{
Name: globals.GCP_INSTANCES_MODULE_NAME,
Header: header,
Body: body,
})
return tableFiles
}
func (g GCPInstancesResults) LootFiles() []internal.LootFile {
// Define any loot files if applicable
return []internal.LootFile{}
}
func runGCPInstancesCommand(cmd *cobra.Command, args []string) {
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_INSTANCES_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_IAM_MODULE_NAME)
}
ces := ComputeEngineService.New()
var results []ComputeEngineService.ComputeEngineInfo
// Set output params leveraging parent (gcp) pflag values
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all instances from project: %s", projectID), globals.GCP_INSTANCES_MODULE_NAME)
result, err := ces.Instances(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_INSTANCES_MODULE_NAME)
return
}
results = append(results, result...)
logger.InfoM(fmt.Sprintf("Done retrieving all instances from project: %s", projectID), globals.GCP_INSTANCES_MODULE_NAME)
cloudfoxOutput := GCPInstancesResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_INSTANCES_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_INSTANCES_MODULE_NAME)
return
}
logger.InfoM("Done writing output", globals.GCP_INSTANCES_MODULE_NAME)
}
}
-120
View File
@@ -1,120 +0,0 @@
package commands
import (
"fmt"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
SecretsService "github.com/BishopFox/cloudfox/gcp/services/secretsService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPSecretsCommand = &cobra.Command{
Use: globals.GCP_SECRETS_MODULE_NAME,
Aliases: []string{},
Short: "Display GCP secrets information",
Args: cobra.MinimumNArgs(0),
Long: `
Display available secrets information:
cloudfox gcp secrets`,
Run: runGCPSecretsCommand,
}
// GCPSecretsResults struct that implements the internal.OutputInterface
type GCPSecretsResults struct {
Data []SecretsService.SecretInfo
}
func (g GCPSecretsResults) TableFiles() []internal.TableFile {
var tableFiles []internal.TableFile
header := []string{
"Name",
"CreationTime",
"Labels",
"Rotation",
"ProjectID",
// Add more fields as necessary
}
var body [][]string
for _, value := range g.Data {
body = append(body, []string{
value.Name,
value.CreationTime,
fmt.Sprintf("%v", value.Labels),
value.Rotation,
value.ProjectID,
})
}
tableFile := internal.TableFile{
Header: header,
Body: body,
Name: globals.GCP_SECRETS_MODULE_NAME,
}
tableFiles = append(tableFiles, tableFile)
return tableFiles
}
func (g GCPSecretsResults) LootFiles() []internal.LootFile {
// Define any specific data considered as loot
return []internal.LootFile{}
}
func runGCPSecretsCommand(cmd *cobra.Command, args []string) {
var projectIDs []string
var account string
parentCmd := cmd.Parent()
ctx := cmd.Context()
logger := internal.NewLogger()
if value, ok := ctx.Value("projectIDs").([]string); ok && len(value) > 0 {
projectIDs = value
} else {
logger.ErrorM("Could not retrieve projectIDs from flag value or value is empty", globals.GCP_SECRETS_MODULE_NAME)
return
}
if value, ok := ctx.Value("account").(string); ok {
account = value
} else {
logger.ErrorM("Could not retrieve account email from command", globals.GCP_IAM_MODULE_NAME)
}
client, err := secretmanager.NewClient(ctx)
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to create secret manager client: %v", err), globals.GCP_SECRETS_MODULE_NAME)
return
}
defer client.Close()
ss := SecretsService.New(client)
var results []SecretsService.SecretInfo
// Set output params from parentCmd
verbosity, _ := parentCmd.PersistentFlags().GetInt("verbosity")
wrap, _ := parentCmd.PersistentFlags().GetBool("wrap")
outputDirectory, _ := parentCmd.PersistentFlags().GetString("outdir")
format, _ := parentCmd.PersistentFlags().GetString("output")
for _, projectID := range projectIDs {
logger.InfoM(fmt.Sprintf("Retrieving all secrets from project: %s", projectID), globals.GCP_SECRETS_MODULE_NAME)
result, err := ss.Secrets(projectID)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_SECRETS_MODULE_NAME)
return
}
results = append(results, result...)
logger.InfoM(fmt.Sprintf("Done retrieving all secrets from project: %s", projectID), globals.GCP_SECRETS_MODULE_NAME)
cloudfoxOutput := GCPSecretsResults{Data: results}
err = internal.HandleOutput("gcp", format, outputDirectory, verbosity, wrap, globals.GCP_SECRETS_MODULE_NAME, account, projectID, cloudfoxOutput)
if err != nil {
logger.ErrorM(err.Error(), globals.GCP_SECRETS_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("Done writing output for project %s", projectID), globals.GCP_SECRETS_MODULE_NAME)
}
}
-34
View File
@@ -1,34 +0,0 @@
package commands
import (
"fmt"
OAuthService "github.com/BishopFox/cloudfox/gcp/services/oauthService"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/spf13/cobra"
)
var GCPWhoAmICommand = &cobra.Command{
Use: globals.GCP_WHOAMI_MODULE_NAME,
Short: "Display the email address of the GCP authenticated user",
Args: cobra.NoArgs,
Run: runGCPWhoAmICommand,
}
func runGCPWhoAmICommand(cmd *cobra.Command, args []string) {
logger := internal.NewLogger()
// Initialize the OAuthService
oauthService := OAuthService.NewOAuthService()
// Call the WhoAmI function
principal, err := oauthService.WhoAmI()
if err != nil {
logger.ErrorM(fmt.Sprintf("Error retrieving token info: %v", err), globals.GCP_WHOAMI_MODULE_NAME)
return
}
logger.InfoM(fmt.Sprintf("authenticated user email: %s", principal.Email), globals.GCP_WHOAMI_MODULE_NAME)
}
@@ -1,236 +0,0 @@
package artifactregistryservice
import (
"context"
"fmt"
"strings"
artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
"github.com/BishopFox/cloudfox/gcp/services/models"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
locationpb "google.golang.org/genproto/googleapis/cloud/location"
)
// New creates a new instance of the ArtifactRegistryService using an artifactregistry.Client
func New(client *artifactregistry.Client) ArtifactRegistryService {
ars := ArtifactRegistryService{
Client: &ArtifactRegistryClientWrapper{
Closer: client.Close,
RepositoryLister: func(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository] {
return client.ListRepositories(ctx, req, opts...)
},
LocationLister: func(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location] {
return client.ListLocations(ctx, req, opts...)
},
RepositoryGetter: func(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error) {
return client.GetRepository(ctx, req, opts...)
},
DockerImageLister: func(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage] {
return client.ListDockerImages(ctx, req, opts...)
},
},
}
return ars
}
var logger internal.Logger
// RepositoriesAndArtifacts retrieves both repositories and their artifacts for a given projectID.
func (ars *ArtifactRegistryService) RepositoriesAndArtifacts(projectID string) (CombinedRepoArtifactInfo, error) {
var combinedInfo CombinedRepoArtifactInfo
// Retrieve repositories.
repos, err := ars.Repositories(projectID)
if err != nil {
return combinedInfo, fmt.Errorf("failed to retrieve repositories: %v", err)
}
combinedInfo.Repositories = repos
// Iterate over repositories to fetch artifacts.
for _, repo := range repos {
// Extract location and repository name from the repository Name.
parts := strings.Split(repo.Name, "/")
if len(parts) < 6 {
logger.InfoM("Unexpected repository name format, skipping artifacts retrieval", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
continue
}
location := parts[3]
repositoryName := parts[5]
// Fetch artifacts for the current repository.
artifacts, err := ars.Artifacts(projectID, location, repositoryName)
if err != nil {
logger.InfoM(fmt.Sprintf("Failed to retrieve artifacts for repository %s: %v", repositoryName, err), globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
continue // Optionally continue to the next repository or handle error differently.
}
combinedInfo.Artifacts = append(combinedInfo.Artifacts, artifacts...)
}
return combinedInfo, nil
}
// RepositoriesAndArtifacts returns a list of repositories and artifacts given projectID.
// TODO consider putting location as input, so that we can put Locations iteration before it & potentially filter by location at top level of GCP module
func (ars *ArtifactRegistryService) Repositories(projectID string) ([]RepositoryInfo, error) {
// Get the list of available locations for the given project.
locations, err := ars.projectLocations(projectID)
if err != nil {
return nil, err
}
var repositories []RepositoryInfo
ctx := context.Background()
// Construct the request for listing repositories.
for _, location := range locations {
req := &artifactregistrypb.ListRepositoriesRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location),
}
it := ars.Client.ListRepositories(ctx, req)
for {
repo, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
repositories = append(repositories, RepositoryInfo{
Name: repo.Name,
Format: repo.Format.String(),
Description: repo.Description,
SizeBytes: fmt.Sprintf("%d", repo.SizeBytes),
ProjectID: projectID,
Location: location,
})
}
}
return repositories, nil
}
// Artifacts fetches the artifacts for a given repository, handling different formats.
func (ars *ArtifactRegistryService) Artifacts(projectID string, location string, repositoryName string) ([]ArtifactInfo, error) {
ctx := context.Background()
// client, err := artifactregistry.NewClient(ctx)
// if err != nil {
// return nil, fmt.Errorf("failed to create artifact registry client: %v", err)
// }
// defer client.Close()
repoFullName := fmt.Sprintf("projects/%s/locations/%s/repositories/%s", projectID, location, repositoryName)
// Fetch repository details to determine its format
repo, err := ars.Client.GetRepository(ctx, &artifactregistrypb.GetRepositoryRequest{Name: repoFullName})
if err != nil {
return nil, fmt.Errorf("failed to get repository details: %v", err)
}
// Handle different repository formats
switch repo.Format {
case artifactregistrypb.Repository_DOCKER:
return ars.DockerImages(repoFullName)
// Placeholder for other formats
// case artifactregistrypb.Repository_MAVEN:
// return listMavenArtifacts(ctx, client, repoFullName)
// case artifactregistrypb.Repository_NPM:
// return listNpmPackages(ctx, client, repoFullName)
default:
logger.InfoM("Repository format is unsupported, so no artifacts were retrieved.", globals.GCP_ARTIFACT_RESGISTRY_MODULE_NAME)
return nil, nil
}
}
// parseDockerImageName parses the Docker image name string and returns the extracted details.
func parseDockerImageName(imageName string) DockerImageDetails {
// Split the image name by '/'
parts := strings.Split(imageName, "/")
// Extract details based on the known structure of the image name.
// Assuming the format is always consistent as described.
projectID := parts[1]
location := parts[3]
repository := parts[5]
// The image name and digest are after the last '/', separated by '@'
imageAndDigest := strings.Split(parts[7], "@")
imageName = imageAndDigest[0]
digest := imageAndDigest[1]
return DockerImageDetails{
ProjectID: projectID,
Location: location,
Repository: repository,
ImageName: imageName,
Digest: digest,
}
}
// listDockerImages specifically lists Docker images within a given repository.
func (ars *ArtifactRegistryService) DockerImages(repositoryName string) ([]ArtifactInfo, error) {
ctx := context.Background()
var artifacts []ArtifactInfo
req := &artifactregistrypb.ListDockerImagesRequest{
Parent: repositoryName,
}
it := ars.Client.ListDockerImages(ctx, req)
for {
image, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
// Parse image name to extract detailed information.
details := parseDockerImageName(image.Name)
// Populate the ArtifactInfo structure with Docker image details.
artifacts = append(artifacts, ArtifactInfo{
Name: details.ImageName,
Format: "DOCKER",
Location: details.Location,
Repository: details.Repository,
SizeBytes: fmt.Sprintf("%d", image.ImageSizeBytes),
Updated: image.UpdateTime.AsTime().String(),
Digest: details.Digest,
ProjectID: details.ProjectID,
})
}
return artifacts, nil
}
// listProjectLocations returns a list of available locations for the given project ID.
func (ars *ArtifactRegistryService) projectLocations(projectID string) ([]string, error) {
// Define the request to list locations.
req := &locationpb.ListLocationsRequest{
Name: fmt.Sprintf("projects/%s", projectID),
}
// Initialize an empty slice to hold the locations.
var locations []string
// Call the ListLocations method and handle pagination.
ctx := context.Background()
it := ars.Client.ListLocations(ctx, req)
for {
loc, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list locations: %w", err)
}
locations = append(locations, loc.LocationId)
}
return locations, nil
}
@@ -1,252 +0,0 @@
package artifactregistryservice_test
import (
"context"
"fmt"
"reflect"
"testing"
artifactRegistryService "github.com/BishopFox/cloudfox/gcp/services/artifactRegistryService"
"github.com/BishopFox/cloudfox/gcp/services/models"
artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
locationpb "google.golang.org/genproto/googleapis/cloud/location"
)
// Mock structure for ArtifactRegistryClientWrapper
type mockArtifactRegistryClient struct {
repositories []*artifactregistrypb.Repository
locations []*locationpb.Location
dockerImages []*artifactregistrypb.DockerImage
}
func (m *mockArtifactRegistryClient) Close() error {
return nil
}
// Mock iterator for repositories
type mockRepoIterator struct {
repositories []*artifactregistrypb.Repository
index int
}
func (it *mockRepoIterator) Next() (*artifactregistrypb.Repository, error) {
if it.index < len(it.repositories) {
repo := it.repositories[it.index]
it.index++
return repo, nil
}
return nil, iterator.Done
}
// Mock iterator for locations
type mockLocationIterator struct {
locations []*locationpb.Location
index int
}
func (it *mockLocationIterator) Next() (*locationpb.Location, error) {
if it.index < len(it.locations) {
loc := it.locations[it.index]
it.index++
return loc, nil
}
return nil, iterator.Done
}
// Mock iterator for Docker images
type mockDockerImageIterator struct {
dockerImages []*artifactregistrypb.DockerImage
index int
}
func (it *mockDockerImageIterator) Next() (*artifactregistrypb.DockerImage, error) {
if it.index < len(it.dockerImages) {
img := it.dockerImages[it.index]
it.index++
return img, nil
}
return nil, iterator.Done
}
// Mock methods
func (m *mockArtifactRegistryClient) ListRepositories(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository] {
return &mockRepoIterator{repositories: m.repositories}
}
func (m *mockArtifactRegistryClient) ListLocations(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location] {
return &mockLocationIterator{locations: m.locations}
}
func (m *mockArtifactRegistryClient) GetRepository(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error) {
for _, repo := range m.repositories {
if repo.Name == req.Name {
return repo, nil
}
}
return nil, fmt.Errorf("repository not found")
}
func (m *mockArtifactRegistryClient) ListDockerImages(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage] {
return &mockDockerImageIterator{dockerImages: m.dockerImages}
}
var mockClient = mockArtifactRegistryClient{}
var ars = artifactRegistryService.ArtifactRegistryService{
Client: &artifactRegistryService.ArtifactRegistryClientWrapper{
Closer: mockClient.Close,
RepositoryLister: func(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository] {
return mockClient.ListRepositories(ctx, req, opts...)
},
LocationLister: func(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location] {
return mockClient.ListLocations(ctx, req, opts...)
},
RepositoryGetter: func(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error) {
return mockClient.GetRepository(ctx, req, opts...)
},
DockerImageLister: func(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage] {
return mockClient.ListDockerImages(ctx, req, opts...)
},
},
}
// Tests go here
func TestArtifacts(t *testing.T) {
cases := []struct {
name string
projectID string
location string
repositoryName string
setupMockClient func(client *mockArtifactRegistryClient)
expectedArtifacts []artifactRegistryService.ArtifactInfo
expectError bool
}{
{
name: "Valid artifacts retrieval",
projectID: "project1",
location: "us-central1",
repositoryName: "repo1",
setupMockClient: func(client *mockArtifactRegistryClient) {
client.dockerImages = []*artifactregistrypb.DockerImage{
{
Name: "projects/project1/locations/us-central1/repositories/repo1/dockerImages/image1@sha256:e9954c1fc875017be1c3e36eca16be2d9e9bccc4bf072163515467d6a823c7cf",
Uri: "us-central1-docker.pkg.dev/project1/repo1/image1@sha256:e9954c1fc875017be1c3e36eca16be2d9e9bccc4bf072163515467d6a823c7cf",
ImageSizeBytes: 1024,
},
}
client.locations = []*locationpb.Location{
{
LocationId: "us-central1",
},
}
client.repositories = []*artifactregistrypb.Repository{{
Name: "projects/project1/locations/us-central1/repositories/repo1",
Format: artifactregistrypb.Repository_DOCKER,
},
}
},
expectedArtifacts: []artifactRegistryService.ArtifactInfo{
{
Name: "image1",
Format: "DOCKER",
Location: "us-central1",
Repository: "repo1",
SizeBytes: "1024",
ProjectID: "project1",
Digest: "sha256:e9954c1fc875017be1c3e36eca16be2d9e9bccc4bf072163515467d6a823c7cf",
Updated: "1970-01-01 00:00:00 +0000 UTC",
},
},
expectError: false,
},
// Add more test cases here
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Reset mockClient state
mockClient.repositories = nil
mockClient.locations = nil
mockClient.dockerImages = nil
// Setup mock client for the current test case
tc.setupMockClient(&mockClient)
artifacts, err := ars.Artifacts(tc.projectID, tc.location, tc.repositoryName)
if (err != nil) != tc.expectError {
t.Errorf("Artifacts() error = %v, expectError %v", err, tc.expectError)
return
}
if !reflect.DeepEqual(artifacts, tc.expectedArtifacts) {
t.Errorf("Artifacts() = %v, expected %v", artifacts, tc.expectedArtifacts)
}
})
}
}
func TestRepositories(t *testing.T) {
cases := []struct {
name string
projectID string
setupMockClient func(client *mockArtifactRegistryClient)
expectedRepositories []artifactRegistryService.RepositoryInfo
expectError bool
}{
{
name: "Valid repositories retrieval",
projectID: "project1",
setupMockClient: func(client *mockArtifactRegistryClient) {
client.repositories = []*artifactregistrypb.Repository{
{
Name: "projects/project1/locations/us-central1/repositories/repo1",
Format: artifactregistrypb.Repository_DOCKER,
Description: "Test repository",
},
}
client.locations = []*locationpb.Location{
{
LocationId: "us-central1",
},
}
},
expectedRepositories: []artifactRegistryService.RepositoryInfo{
{
Name: "projects/project1/locations/us-central1/repositories/repo1",
Format: "DOCKER",
Description: "Test repository",
SizeBytes: "0",
ProjectID: "project1",
Location: "us-central1",
},
},
expectError: false,
},
// Add more test cases here
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Reset mockClient state
mockClient.repositories = nil
mockClient.locations = nil
mockClient.dockerImages = nil
// Setup mock client for the current test case
tc.setupMockClient(&mockClient)
repositories, err := ars.Repositories(tc.projectID)
if (err != nil) != tc.expectError {
t.Errorf("Repositories() error = %v, expectError %v", err, tc.expectError)
return
}
if !reflect.DeepEqual(repositories, tc.expectedRepositories) {
t.Errorf("Repositories() = %v, expected %v", repositories, tc.expectedRepositories)
}
})
}
}
@@ -1,78 +0,0 @@
package artifactregistryservice
import (
"context"
artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
"github.com/BishopFox/cloudfox/gcp/services/models"
"github.com/googleapis/gax-go/v2"
locationpb "google.golang.org/genproto/googleapis/cloud/location"
)
// CombinedRepoArtifactInfo holds the combined information of repositories and their respective artifacts.
type CombinedRepoArtifactInfo struct {
Repositories []RepositoryInfo `json:"repositories"`
Artifacts []ArtifactInfo `json:"artifacts"`
}
// ArtifactInfo represents the basic information of an artifact within a registry.
type ArtifactInfo struct {
Name string `json:"name"`
Format string `json:"format"`
Version string `json:"version"`
Location string `json:"location"`
Repository string `json:"repository"`
SizeBytes string `json:"virtualSize"`
Updated string `json:"updated"`
Digest string `json:"digest"`
ProjectID string `json:"projectID"`
}
// RepositoryInfo holds information about a repository and its artifacts.
type RepositoryInfo struct {
Name string `json:"name"`
Format string `json:"format"`
Description string `json:"description"`
SizeBytes string `json:"sizeBytes"`
ProjectID string `json:"projectID"`
Location string `json:"location"`
}
// DockerImageDetails holds the extracted parts from a Docker image name.
type DockerImageDetails struct {
ProjectID string `json:"projectID"`
Location string `json:"location"`
Repository string `json:"repository"`
ImageName string `json:"name"`
Digest string `json:"digest"`
}
// Function calls to the API are wrapped to facilitate mocking
type ArtifactRegistryClientWrapper struct {
Closer func() error
RepositoryLister func(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository]
LocationLister func(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location]
RepositoryGetter func(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error)
DockerImageLister func(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage]
}
func (w *ArtifactRegistryClientWrapper) ListRepositories(ctx context.Context, req *artifactregistrypb.ListRepositoriesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.Repository] {
return w.RepositoryLister(ctx, req, opts...)
}
func (w *ArtifactRegistryClientWrapper) ListLocations(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) models.GenericIterator[locationpb.Location] {
return w.LocationLister(ctx, req, opts...)
}
func (w *ArtifactRegistryClientWrapper) GetRepository(ctx context.Context, req *artifactregistrypb.GetRepositoryRequest, opts ...gax.CallOption) (*artifactregistrypb.Repository, error) {
return w.RepositoryGetter(ctx, req, opts...)
}
func (w *ArtifactRegistryClientWrapper) ListDockerImages(ctx context.Context, req *artifactregistrypb.ListDockerImagesRequest, opts ...gax.CallOption) models.GenericIterator[artifactregistrypb.DockerImage] {
return w.DockerImageLister(ctx, req, opts...)
}
// ArtifactRegistryService provides methods to interact with Artifact Registry resources.
type ArtifactRegistryService struct {
Client *ArtifactRegistryClientWrapper
}
@@ -1,150 +0,0 @@
package bigqueryservice
import (
"context"
"time"
"cloud.google.com/go/bigquery"
"google.golang.org/api/iterator"
)
// BigqueryDataset represents a dataset in BigQuery
type BigqueryDataset struct {
DatasetID string
Location string
CreationTime time.Time
LastModifiedTime time.Time
Description string
Name string
ProjectID string
}
// BigqueryTable represents a table in BigQuery
type BigqueryTable struct {
TableID string
DatasetID string
Location string
CreationTime time.Time
LastModifiedTime time.Time
NumBytes int64
Description string
ProjectID string
}
// CombinedBigqueryData represents both datasets and tables within a project
type CombinedBigqueryData struct {
Datasets []BigqueryDataset
Tables []BigqueryTable
}
type BigQueryService struct {
// Placeholder for any required services or configuration
}
// New creates a new instance of BigQueryService
func New() *BigQueryService {
return &BigQueryService{}
}
// gcloud alpha bq datasets list
// gcloud alpha bq datasets describe terragoat_dev_dataset
// gcloud alpha bq tables list --dataset terragoat_dev_dataset
// gcloud alpha bq tables describe bar --dataset terragoat_dev_dataset
// BigqueryDatasetsAndTables retrieves all datasets and their tables for a given projectID
func (bq *BigQueryService) BigqueryDatasetsAndTables(projectID string) (CombinedBigqueryData, error) {
// Initialize the combined struct to hold datasets and tables
combinedData := CombinedBigqueryData{}
// Retrieve all datasets
datasets, err := bq.BigqueryDatasets(projectID)
if err != nil {
return combinedData, err
}
combinedData.Datasets = datasets
// Iterate over each dataset to retrieve its tables
for _, dataset := range datasets {
tables, err := bq.BigqueryTables(projectID, dataset.DatasetID)
if err != nil {
return combinedData, err
}
combinedData.Tables = append(combinedData.Tables, tables...)
}
return combinedData, nil
}
// BigqueryDatasets retrieves datasets from the given projectID across all locations
func (bq *BigQueryService) BigqueryDatasets(projectID string) ([]BigqueryDataset, error) {
ctx := context.Background()
client, err := bigquery.NewClient(ctx, projectID)
if err != nil {
return nil, err
}
defer client.Close()
var datasets []BigqueryDataset
it := client.Datasets(ctx)
for {
ds, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
meta, err := ds.Metadata(ctx)
if err != nil {
return nil, err
}
datasets = append(datasets, BigqueryDataset{
DatasetID: ds.DatasetID,
Location: meta.Location,
CreationTime: meta.CreationTime,
LastModifiedTime: meta.LastModifiedTime,
Description: meta.Description,
Name: meta.Name,
ProjectID: projectID,
})
}
return datasets, nil
}
// BigqueryTables retrieves tables from the given projectID and dataset across all locations
func (bq *BigQueryService) BigqueryTables(projectID string, datasetID string) ([]BigqueryTable, error) {
ctx := context.Background()
client, err := bigquery.NewClient(ctx, projectID)
if err != nil {
return nil, err
}
defer client.Close()
var tables []BigqueryTable
ds := client.Dataset(datasetID)
it := ds.Tables(ctx)
for {
table, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
meta, err := table.Metadata(ctx)
if err != nil {
return nil, err
}
tables = append(tables, BigqueryTable{
TableID: table.TableID,
DatasetID: datasetID,
Location: meta.Location,
CreationTime: meta.CreationTime,
LastModifiedTime: meta.LastModifiedTime,
NumBytes: meta.NumBytes,
Description: meta.Description,
ProjectID: projectID,
})
}
return tables, nil
}
@@ -1 +0,0 @@
package bigqueryservice_test
@@ -1,117 +0,0 @@
package cloudstorageservice
import (
"context"
"fmt"
"cloud.google.com/go/storage"
"google.golang.org/api/iterator"
)
type CloudStorageService struct {
// DataStoreService datastoreservice.DataStoreService
}
func New() *CloudStorageService {
return &CloudStorageService{}
}
// type ObjectInfo struct {
// ObjectName string `json:"objecttName"`
// ObjectSizeBytes float64 `json:"objectSizeBytes"`
// IsPublic bool `json:"isPublic"`
// }
type BucketInfo struct {
Name string `json:"name"`
Location string `json:"location"`
ProjectID string `json:"projectID"`
}
func (cs *CloudStorageService) Buckets(projectID string) ([]BucketInfo, error) {
ctx := context.Background()
client, err := storage.NewClient(ctx)
if err != nil {
return nil, fmt.Errorf("Failed to create client: %v", err)
}
defer client.Close()
var buckets []BucketInfo
bucketIterator := client.Buckets(ctx, projectID)
for {
battrs, err := bucketIterator.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
bucket := BucketInfo{Name: battrs.Name, Location: battrs.Location, ProjectID: projectID}
buckets = append(buckets, bucket)
}
return buckets, nil
}
// func (cs *CloudStorageService) BucketsWithMetaData(projectID string) (map[string][]BucketInfo, error) {
// buckets, _ := cs.Buckets(projectID)
// bucketInfos := make(map[string][]BucketInfo)
// ctx := context.Background()
// client, err := storage.NewClient(ctx)
// if err != nil {
// return nil, fmt.Errorf("Failed to create client: %v", err)
// }
// for {
// bucketAttrs, err := buckets.Next()
// if err == iterator.Done {
// break
// }
// if err != nil {
// return nil, fmt.Errorf("failed to list buckets: %v", err)
// }
// bucketName := bucketAttrs.Name
// log.Printf("Working on bucket %s", bucketName)
// // List all objects in the bucket and calculate total size
// totalSize := int64(0)
// var objects []ObjectInfo
// it := client.Bucket(bucketName).Objects(ctx, nil)
// for {
// objectAttrs, err := it.Next()
// if err == iterator.Done {
// break
// }
// if err != nil {
// return nil, fmt.Errorf("failed to list objects in bucket %s: %v", bucketName, err)
// }
// // Get size
// objectSize := objectAttrs.Size
// totalSize += objectSize
// // Check if public
// isPublic := false
// for _, rule := range objectAttrs.ACL {
// if rule.Entity == storage.AllUsers {
// isPublic = true
// break
// }
// }
// objects = append(objects, ObjectInfo{ObjectName: objectAttrs.Name, ObjectSizeBytes: float64(objectSize), IsPublic: isPublic})
// if totalSize > 3221225472 { // 3 GiB in bytes
// log.Printf("%s bucket is over 3 GiB. Skipping remaining objects in this bucket...", bucketName)
// break
// }
// }
// bucketSizeMB := float64(totalSize) / 1024 / 1024
// bucketInfos[projectID] = append(bucketInfos[projectID], BucketInfo{BucketName: bucketName, BucketSizeMB: bucketSizeMB, Objects: objects})
// }
// log.Printf("Sorting resulting list of buckets in descending order %s", projectID)
// sort.Slice(bucketInfos[projectID], func(i, j int) bool {
// return bucketInfos[projectID][i].BucketSizeMB > bucketInfos[projectID][j].BucketSizeMB
// })
// return bucketInfos, nil
// }
@@ -1 +0,0 @@
package cloudstorageservice_test
@@ -1,99 +0,0 @@
package computeengineservice
import (
"context"
"fmt"
"strings"
"google.golang.org/api/compute/v1"
)
type ComputeEngineService struct {
// DataStoreService datastoreservice.DataStoreService
}
func New() *ComputeEngineService {
return &ComputeEngineService{}
}
type ComputeEngineInfo struct {
Name string
ID string
Zone string
State string
ExternalIP string
InternalIP string
ServiceAccounts []*compute.ServiceAccount // Assuming role is derived from service accounts
NetworkInterfaces []*compute.NetworkInterface
Tags *compute.Tags
ProjectID string
}
// Retrieves instances from all regions and zones for a project without using concurrency.
func (ces *ComputeEngineService) Instances(projectID string) ([]ComputeEngineInfo, error) {
ctx := context.Background()
computeService, err := compute.NewService(ctx)
if err != nil {
return nil, err
}
regions, err := computeService.Regions.List(projectID).Do()
if err != nil {
return nil, err
}
var instanceInfos []ComputeEngineInfo
for _, region := range regions.Items {
for _, zoneURL := range region.Zones {
zone := getZoneNameFromURL(zoneURL)
instanceList, err := computeService.Instances.List(projectID, zone).Do()
if err != nil {
return nil, fmt.Errorf("error retrieving instances from zone %s: %v", zone, err)
}
for _, instance := range instanceList.Items {
info := ComputeEngineInfo{
Name: instance.Name,
ID: fmt.Sprintf("%v", instance.Id),
Zone: zoneURL,
State: instance.Status,
ExternalIP: getExternalIP(instance),
InternalIP: getInternalIP(instance),
ServiceAccounts: instance.ServiceAccounts,
NetworkInterfaces: instance.NetworkInterfaces,
Tags: instance.Tags,
ProjectID: projectID,
}
instanceInfos = append(instanceInfos, info)
}
}
}
return instanceInfos, nil
}
// Returns the zone from a GCP URL string with the zone in it
func getZoneNameFromURL(zoneURL string) string {
splits := strings.Split(zoneURL, "/")
return splits[len(splits)-1]
}
// getExternalIP extracts the external IP address from an instance if available.
func getExternalIP(instance *compute.Instance) string {
for _, iface := range instance.NetworkInterfaces {
for _, accessConfig := range iface.AccessConfigs {
if accessConfig.NatIP != "" {
return accessConfig.NatIP
}
}
}
return ""
}
// getInternalIP extracts the internal IP address from an instance.
func getInternalIP(instance *compute.Instance) string {
if len(instance.NetworkInterfaces) > 0 {
return instance.NetworkInterfaces[0].NetworkIP
}
return ""
}
// TODO consider just getting the emails of the service account and returning a []string
@@ -1 +0,0 @@
package computeengineservice
-93
View File
@@ -1,93 +0,0 @@
package iamservice
// import (
// "fmt"
// "strings"
// "time"
// "github.com/fatih/color"
// "github.com/BishopFox/cloudfox/internal/gcp"
// "github.com/BishopFox/cloudfox/internal"
// "github.com/BishopFox/cloudfox/globals"
// )
// type AccessTokensModule struct {
// // Filtering data
// Organizations []string
// Folders []string
// Projects []string
// }
// // Be incorportated in iam.go
// func (m *AccessTokensModule) PrintAccessTokens(outputFormat string, outputDirectory string, verbosity int) error {
// GCPLogger.InfoM(fmt.Sprintf("Enumerating GCP local access tokens (%s, %s)...", color.CyanString("default user token"), color.RedString("application-default token")), globals.GCP_ACCESSTOKENS_MODULE_NAME)
// tokens := gcp.ReadRefreshTokens()
// accessTokens := gcp.ReadAccessTokens()
// applicationdefaulthash := gcp.GetDefaultApplicationHash()
// activeAccount := gcp.GetActiveAccount()
// var tableHead = []string{"Account", "Credential Type", "Validity", "Refresh Token", "Refresh Token needs password"}
// var tableBody [][]string
// for _, accessToken := range accessTokens {
// // prepare token's associated ID (email or hash)
// var emailinfo string
// if (accessToken.AccountID == activeAccount) {
// emailinfo = color.BlueString(accessToken.AccountID)
// } else {
// emailinfo = accessToken.AccountID
// }
// // format expiration time
// Exp, _ := time.Parse(time.RFC3339, accessToken.TokenExpiry)
// var timeinfo string
// if Exp.After(time.Now()) {
// timeinfo = Exp.Format(time.RFC1123)
// } else {
// timeinfo = "EXPIRED"
// }
// // display token type, user or application credential
// var tokentype string
// if (strings.Contains(accessToken.AccountID, "@")) {
// tokentype = "User"
// } else {
// tokentype = "Application"
// }
// // is there an associated refres htoken ?
// var refreshtokeninfo string
// refreshtokeninfo = "No"
// for _, refreshtoken := range tokens {
// if (accessToken.AccountID == refreshtoken.Email) {
// refreshtokeninfo = "Yes"
// } else if (accessToken.AccountID == applicationdefaulthash) {
// refreshtokeninfo = "Yes"
// emailinfo = color.RedString(emailinfo)
// }
// }
// // is there a proof of reauthentication to use the refresh
// // token ? If not, password is needed
// var raptinfo string
// if refreshtokeninfo == "No" {
// raptinfo = "-"
// } else if accessToken.RaptToken.Valid {
// raptinfo = "No"
// } else {
// if (tokentype == "Application") {
// raptinfo = "TBD"
// } else {
// raptinfo = "Yes"
// }
// }
// tableBody = append(
// tableBody,
// []string{
// emailinfo,
// tokentype,
// timeinfo,
// refreshtokeninfo,
// raptinfo,
// })
// }
// internal.PrintTableToScreen(tableHead, tableBody, false)
// return nil
// }
-189
View File
@@ -1,189 +0,0 @@
package iamservice
import (
"context"
"fmt"
"strings"
iampb "cloud.google.com/go/iam/apiv1/iampb"
resourcemanager "cloud.google.com/go/resourcemanager/apiv3"
resourcemanagerpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
"github.com/BishopFox/cloudfox/globals"
"github.com/BishopFox/cloudfox/internal"
)
type IAMService struct {
// DataStoreService datastoreservice.DataStoreService
}
func New() *IAMService {
return &IAMService{}
}
// AncestryResource represents a single resource in the project's ancestry.
type AncestryResource struct {
Type string `json:"type"`
Id string `json:"id"`
}
// PolicyBindings represents IAM policy bindings.
type PolicyBinding struct {
Role string `json:"role"`
Members []string `json:"members"`
ResourceID string `json:"resourceID"`
ResourceType string
PolicyName string `json:"policyBindings"`
Condition string
}
type PrincipalWithRoles struct {
Name string
Type string
PolicyBindings []PolicyBinding
ResourceID string
ResourceType string
}
var logger internal.Logger
func projectAncestry(projectID string) ([]AncestryResource, error) {
ctx := context.Background()
projectsClient, err := resourcemanager.NewProjectsClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create projects client: %v", err)
}
defer projectsClient.Close()
foldersClient, err := resourcemanager.NewFoldersClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create folders client: %v", err)
}
defer foldersClient.Close()
resourceID := "projects/" + projectID
var ancestry []AncestryResource
for {
if strings.HasPrefix(resourceID, "organizations/") {
ancestry = append(ancestry, AncestryResource{Type: "organization", Id: strings.TrimPrefix(resourceID, "organizations/")})
break
} else if strings.HasPrefix(resourceID, "folders/") {
resp, err := foldersClient.GetFolder(ctx, &resourcemanagerpb.GetFolderRequest{Name: resourceID})
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to access folder %s, %v", resourceID, err), globals.GCP_IAM_MODULE_NAME)
break // Stop processing further if a folder is inaccessible
}
ancestry = append(ancestry, AncestryResource{Type: "folder", Id: strings.TrimPrefix(resp.Name, "folders/")})
resourceID = resp.Parent
} else if strings.HasPrefix(resourceID, "projects/") {
resp, err := projectsClient.GetProject(ctx, &resourcemanagerpb.GetProjectRequest{Name: resourceID})
if err != nil {
logger.ErrorM(fmt.Sprintf("failed to access project %s, %v", resourceID, err), globals.GCP_IAM_MODULE_NAME)
return nil, fmt.Errorf("failed to get project: %v", err)
}
ancestry = append(ancestry, AncestryResource{Type: "project", Id: strings.TrimPrefix(resp.Name, "projects/")})
resourceID = resp.Parent
} else {
return nil, fmt.Errorf("unknown resource type for: %s", resourceID)
}
}
// Reverse the slice as we've built it from child to ancestor
for i, j := 0, len(ancestry)-1; i < j; i, j = i+1, j-1 {
ancestry[i], ancestry[j] = ancestry[j], ancestry[i]
}
return ancestry, nil
}
// Policies fetches IAM policy for a given resource and all policies in resource ancestry
func (s *IAMService) Policies(resourceID string, resourceType string) ([]PolicyBinding, error) {
ctx := context.Background()
client, err := resourcemanager.NewProjectsClient(ctx)
if err != nil {
return nil, fmt.Errorf("resourcemanager.NewProjectsClient: %v", err)
}
defer client.Close()
var resourceName string
switch resourceType {
case "project":
resourceName = "projects/" + resourceID
case "folder":
resourceName = "folders/" + resourceID
case "organization":
resourceName = "organizations/" + resourceID
default:
return nil, fmt.Errorf("unsupported resource type: %s", resourceType)
}
req := &iampb.GetIamPolicyRequest{
Resource: resourceName,
}
// Fetch the IAM policy for the resource
policy, err := client.GetIamPolicy(ctx, req)
if err != nil {
return nil, fmt.Errorf("client.GetIamPolicy: %v", err)
}
// Assemble the policy bindings
var policyBindings []PolicyBinding
for _, binding := range policy.Bindings {
policyBinding := PolicyBinding{
Role: binding.Role,
Members: binding.Members,
ResourceID: resourceID,
ResourceType: resourceType,
Condition: binding.Condition.String(),
PolicyName: resourceName + "_policyBindings",
}
policyBindings = append(policyBindings, policyBinding)
}
return policyBindings, nil
}
func determinePrincipalType(member string) string {
if strings.HasPrefix(member, "user:") {
return "User"
} else if strings.HasPrefix(member, "serviceAccount:") {
return "ServiceAccount"
} else if strings.HasPrefix(member, "group:") {
return "Group"
} else {
return "Unknown"
}
}
func (s *IAMService) PrincipalsWithRoles(resourceID string, resourceType string) ([]PrincipalWithRoles, error) {
policyBindings, err := s.Policies(resourceID, resourceType)
if err != nil {
return nil, err
}
principalMap := make(map[string]*PrincipalWithRoles)
for _, pb := range policyBindings {
for _, member := range pb.Members {
principalType := determinePrincipalType(member) // Implement this function based on member prefix
if principal, ok := principalMap[member]; ok {
principal.PolicyBindings = append(principal.PolicyBindings, pb)
} else {
principalMap[member] = &PrincipalWithRoles{
Name: member,
Type: principalType,
PolicyBindings: []PolicyBinding{pb},
ResourceID: resourceID,
ResourceType: resourceType,
}
}
}
}
var principals []PrincipalWithRoles
for _, principal := range principalMap {
principals = append(principals, *principal)
}
return principals, nil
}
@@ -1 +0,0 @@
package iamservice_test
-5
View File
@@ -1,5 +0,0 @@
package models
type GenericIterator[T any] interface {
Next() (*T, error)
}
@@ -1,260 +0,0 @@
package networkservice
import (
"context"
"log"
"net"
"strings"
ComputeEngineService "github.com/BishopFox/cloudfox/gcp/services/computeEngineService"
"google.golang.org/api/compute/v1"
)
// VPC / network / subnets
// gcloud compute networks list
// gcloud compute networks get-effective-firewalls terragoat-dev-network
// gcloud compute networks subnets list
// FirewallRule structure for JSON output
// firewall-rules
// gcloud compute firewall-rules list - `terragoat-dev-firewall`
// forwarding rules (load balancers)
// gcloud compute forwarding-rules list
// Router & NAT gateways
// gcloud compute routers list
// gcloud compute routers nats list --router
// Global network firewall policy
// gcloud compute network-firewall-policies
// gcloud compute network-firewall-policies rules list
// gcloud compute network-firewall-policies associations list
type FirewallRule struct {
FWName string `json:"fw_name"`
Description string `json:"description"`
Direction string `json:"direction"`
Allowed map[string][]string `json:"allowed"`
Denied map[string][]string `json:"denied"`
SourceRanges []string `json:"source_ranges"`
TargetTag string `json:"target_tag"`
AllowedTraffic Traffic `json:"allowed_traffic"`
ExposedEndpoints []Endpoint `json:"exposed_endpoints"`
DestinationRanges []string `json:"destinatioRanges"`
TargetTags []string `json:"targetTags"`
}
type Traffic struct {
TCP []string `json:"tcp"`
UDP []string `json:"udp"`
}
type Endpoint struct {
IP string `json:"ip"`
Ports []string `json:"ports"`
Protocol string `json:"protocol"`
}
type NetwworkService struct {
// DataStoreService datastoreservice.DataStoreService
}
func New() *NetwworkService {
return &NetwworkService{}
}
// Returns firewall rules for a project.
func (ns *NetwworkService) FirewallRules(projectID string) ([]*compute.Firewall, error) {
ctx := context.Background()
computeService, err := compute.NewService(ctx)
if err != nil {
return nil, err
}
firewallList, err := computeService.Firewalls.List(projectID).Do()
if err != nil {
return nil, err
}
return firewallList.Items, nil
}
// Returns firewall rules with additional logic to tag rules that expose resources to the public
func (ns *NetwworkService) FirewallRulesWithPublicExposure(projectID string) ([]FirewallRule, error) {
var results []FirewallRule
log.Printf("Retrieving firewall rules for project %s", projectID)
firewalls, err := ns.FirewallRules(projectID)
log.Print("Done")
if err != nil {
log.Printf("Error fetching firewall rules for project %s: %v", projectID, err)
return nil, err
}
log.Print("Parsing firewall rules")
for _, fw := range firewalls {
parsedRule, err := parseFirewallRule(fw, projectID)
if err != nil {
log.Printf("Error parsing firewall rule for project %s: %v", projectID, err)
continue
}
results = append(results, parsedRule)
}
log.Print("Done")
return results, nil
}
// Returns a list of IPs that are mapped to a given tag. Uses a list of instances
func getIPAddressesForTargetTag(instances []ComputeEngineService.ComputeEngineInfo, tag string) ([]string, error) {
var ips []string
for _, instance := range instances {
if contains(instance.Tags.Items, tag) {
ips = append(ips, instance.NetworkInterfaces[0].NetworkIP)
}
}
return ips, nil
}
// Returns the zone from a GCP URL string with the zone in it
func getZoneNameFromURL(zoneURL string) string {
splits := strings.Split(zoneURL, "/")
return splits[len(splits)-1]
}
// Returns true of a string is in the given list of strings. Else false
func contains(slice []string, item string) bool {
for _, a := range slice {
if a == item {
return true
}
}
return false
}
// Returns true if a string in compute.Firewall.SourceRanges is "0.0.0.0/0"
func isExposedToInternet(fw *compute.Firewall) bool {
for _, rangeVal := range fw.SourceRanges {
if rangeVal == "0.0.0.0/0" {
return true
}
}
return false
}
// Returns true if the IP is an internal ipv4 or ipv6 IP
func IsInternalIP(cidr string) bool {
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
// Try parsing as just an IP (not CIDR)
ip = net.ParseIP(cidr)
if ip == nil {
log.Printf("Error parsing input %s\n", cidr)
return false
}
}
// Check for IPv4 private ranges
if ip.To4() != nil {
for _, privateCIDR := range []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
} {
_, block, _ := net.ParseCIDR(privateCIDR)
if block.Contains(ip) {
return true
}
}
return false
}
// Check for IPv6 ULA range fc00::/7
// This checks if the first byte of the address is in the range 0xfc to 0xfd
if ip[0] == 0xfc || ip[0] == 0xfd {
return true
}
return false
}
// getPublicEndpoints fetches the endpoints exposed to the internet
// and, if a TargetTag is provided, gets the IP associated with it.
func getPublicEndpoints(fw *compute.Firewall, projectID string) ([]Endpoint, error) {
var exposedEndpoints []Endpoint
ces := ComputeEngineService.New()
if isExposedToInternet(fw) {
for _, allowed := range fw.Allowed {
// Handle destination ranges
for _, destRange := range fw.DestinationRanges {
if !IsInternalIP(destRange) {
exposed := Endpoint{
IP: destRange,
Ports: allowed.Ports,
Protocol: allowed.IPProtocol,
}
exposedEndpoints = append(exposedEndpoints, exposed)
}
}
// If TargetTag is provided, fetch the associated IP addresses.
if len(fw.TargetTags) > 0 {
log.Printf("Target tags found, getting their IP...")
instances, err := ces.Instances(projectID)
if err != nil {
log.Printf("Error retrieving instances: %v", err)
return nil, err
}
for _, tag := range fw.TargetTags {
ips, err := getIPAddressesForTargetTag(instances, tag)
if err != nil {
return nil, err
}
for _, ip := range ips {
if !IsInternalIP(ip) {
exposed := Endpoint{
IP: ip,
Ports: allowed.Ports,
Protocol: allowed.IPProtocol,
}
exposedEndpoints = append(exposedEndpoints, exposed)
}
}
}
}
}
}
return exposedEndpoints, nil
}
// The original parseFirewallRule function
func parseFirewallRule(fw *compute.Firewall, projectID string) (FirewallRule, error) {
var exposedEndpoints []Endpoint
trafficAllowed := make(map[string][]string)
trafficDenied := make(map[string][]string)
for _, allowed := range fw.Allowed {
trafficAllowed[allowed.IPProtocol] = append(trafficAllowed[allowed.IPProtocol], allowed.Ports...)
}
exposedEndpoints, err := getPublicEndpoints(fw, projectID)
if err != nil {
return FirewallRule{}, err
}
for _, denied := range fw.Denied {
trafficDenied[denied.IPProtocol] = append(trafficDenied[denied.IPProtocol], denied.Ports...)
}
return FirewallRule{
FWName: fw.Name,
Description: fw.Description,
Direction: fw.Direction,
Allowed: trafficAllowed,
Denied: trafficDenied,
SourceRanges: fw.SourceRanges,
TargetTag: strings.Join(fw.TargetTags, ","),
ExposedEndpoints: exposedEndpoints,
}, nil
}
// TODO
// func (ns *NetworkService) ForwardingRules() {}
@@ -1,35 +0,0 @@
package networkservice_test
import (
"testing"
networkservice "github.com/BishopFox/cloudfox/gcp/services/networkService"
)
func TestIsInternalIP(t *testing.T) {
tests := []struct {
input string
expect bool
}{
// IPv4 tests
{"10.1.2.3", true}, // falls within 10.0.0.0/8
{"172.17.0.1", true}, // falls within 172.16.0.0/12
{"192.168.10.1", true}, // falls within 192.168.0.0/16
{"11.1.2.3", false}, // public IP
{"172.32.0.1", false}, // public IP
{"192.169.0.1", false}, // public IP
{"8.8.8.8", false}, // public IP
// IPv6 tests
{"fc00::1", true}, // falls within fc00::/7
{"fd00::1", true}, // falls within fd00::/8
{"fe80::1", false}, // link-local address, not ULA
{"2001:0db8:85a3::8a2e", false}, // public IP
}
for _, test := range tests {
result := networkservice.IsInternalIP(test.input)
if result != test.expect {
t.Errorf("IsInternalIP(%s) = %v; want %v", test.input, result, test.expect)
}
}
}
-84
View File
@@ -1,84 +0,0 @@
package oauthservice
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"golang.org/x/oauth2/google"
)
// Principal struct modified to map JSON response fields.
type Principal struct {
Email string `json:"email"`
Scope []string
}
// Temporary struct to match the JSON structure.
type tokenInfoResponse struct {
Email string `json:"email"`
Scope string `json:"scope"` // Keep as string to match JSON.
}
// OAuthService struct definition (unchanged)
type OAuthService struct {
// This struct can be expanded in the future if necessary.
}
// NewOAuthService creates a new instance of OAuthService.
func NewOAuthService() *OAuthService {
return &OAuthService{}
}
// WhoAmI function returns the Principal struct showing the email and scope of the currently authenticated user by generating an oauth token
func (s *OAuthService) WhoAmI() (*Principal, error) {
ctx := context.Background()
ts, err := google.DefaultTokenSource(ctx, "")
if err != nil {
return nil, fmt.Errorf("failed to obtain default token source: %v", err)
}
token, err := ts.Token()
if err != nil {
return nil, fmt.Errorf("failed to get token from token source: %v", err)
}
tokenInfo, err := queryTokenInfo(token.AccessToken)
if err != nil {
return nil, fmt.Errorf(fmt.Sprintf("failed to retrieve metada of the token with error: %s", err.Error()))
}
// Split the scope string into a slice of strings.
scopes := strings.Split(tokenInfo.Scope, " ")
return &Principal{
Email: tokenInfo.Email,
Scope: scopes,
}, nil
}
// queryTokenInfo function modified to return (*Principal, error).
func queryTokenInfo(accessToken string) (*tokenInfoResponse, error) {
url := fmt.Sprintf("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s", accessToken)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("error making request to tokeninfo endpoint: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading tokeninfo response body: %v", err)
}
var response tokenInfoResponse
// Unmarshal the JSON response body into the Principal struct.
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("error unmarshalling tokeninfo response body: %v", err)
}
return &response, nil
}
@@ -1,83 +0,0 @@
package secretservice
import (
"context"
"fmt"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
)
// Wrappers and abstracting types to facilitate mocking the client responses
type Iterator interface {
Next() (*secretmanagerpb.Secret, error)
}
type SecretsManagerClientWrapper struct {
Closer func() error
SecretLister func(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) Iterator
}
func (w *SecretsManagerClientWrapper) Close() error {
return w.Closer()
}
func (w *SecretsManagerClientWrapper) ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) Iterator {
return w.SecretLister(ctx, req, opts...)
}
type SecretsService struct {
Client *SecretsManagerClientWrapper
}
// New function to facilitate using the ss client
func New(client *secretmanager.Client) SecretsService {
ss := SecretsService{
Client: &SecretsManagerClientWrapper{
Closer: client.Close,
SecretLister: func(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) Iterator {
return client.ListSecrets(ctx, req, opts...)
},
},
}
return ss
}
type SecretInfo struct {
Name string `json:"name"`
ProjectID string `json:"projectID"`
CreationTime string `json:"creationTime"`
Labels map[string]string `json:"labels"`
Rotation string `json:"rotation,omitempty"`
}
func (ss *SecretsService) Secrets(projectID string) ([]SecretInfo, error) {
var secrets []SecretInfo
req := &secretmanagerpb.ListSecretsRequest{
Parent: fmt.Sprintf("projects/%s", projectID),
}
ctx := context.Background()
it := ss.Client.ListSecrets(ctx, req)
for {
resp, err := it.Next() //Here it errors out
if err == iterator.Done {
break
}
if err != nil {
return nil, fmt.Errorf("failed to list secrets: %v", err)
}
secrets = append(secrets, SecretInfo{
Name: resp.Name,
ProjectID: projectID,
CreationTime: resp.CreateTime.AsTime().String(),
Labels: resp.Labels,
Rotation: resp.Rotation.String(),
})
}
return secrets, nil
}
@@ -1,157 +0,0 @@
package secretservice_test
import (
"context"
"reflect"
"testing"
"time"
secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
secretservice "github.com/BishopFox/cloudfox/gcp/services/secretsService"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
"google.golang.org/protobuf/types/known/timestamppb"
)
type mockSecretManagerClient struct {
secrets []*secretmanagerpb.Secret
}
func (m *mockSecretManagerClient) Close() error {
return nil
}
// SecretIterator simulates the iterator over secrets.
type SecretIterator struct {
secrets []*secretmanagerpb.Secret // Current secrets batch
nextIndex int // Index of the next secret to return
InternalFetch func(pageSize int, pageToken string) (results []*secretmanagerpb.Secret, nextPageToken string, err error)
currentPageToken string
}
func (it *SecretIterator) Next() (*secretmanagerpb.Secret, error) {
// If we have reached the end of the current secrets batch, attempt to fetch the next batch
if it.nextIndex >= len(it.secrets) {
var err error
it.secrets, it.currentPageToken, err = it.InternalFetch(10, it.currentPageToken) // Assuming a fixed page size for simplicity
if err != nil {
return nil, err // Propagate errors from fetch
}
it.nextIndex = 0 // Reset the index for the new batch
// If the fetched batch is empty, we have reached the end of the iteration
if len(it.secrets) == 0 {
return nil, iterator.Done
}
}
// Retrieve the next secret and increment the index
secret := it.secrets[it.nextIndex]
it.nextIndex++
return secret, nil
}
// ListSecrets simulates the ListSecrets method of the SecretManagerClient interface.
func (m *mockSecretManagerClient) ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) secretservice.Iterator {
it := &SecretIterator{}
// Copy secrets to avoid modifying the original slice
allSecrets := make([]*secretmanagerpb.Secret, len(m.secrets))
copy(allSecrets, m.secrets)
// Simulate internal fetch by setting InternalFetch to a function that returns the next set of secrets
nextIndex := 0
it.InternalFetch = func(pageSize int, pageToken string) (results []*secretmanagerpb.Secret, nextPageToken string, err error) {
// Check if we have reached the end of the secrets slice
if nextIndex >= len(allSecrets) {
return nil, "", iterator.Done
}
// Determine the slice of secrets to return
endIndex := nextIndex + pageSize
if endIndex > len(allSecrets) {
endIndex = len(allSecrets)
}
results = allSecrets[nextIndex:endIndex]
nextIndex = endIndex
// Simulate the nextPageToken logic (omitted for brevity, always return empty string in this mock)
nextPageToken = ""
return results, nextPageToken, nil
}
return it
}
func TestSecrets(t *testing.T) {
mockClient := &mockSecretManagerClient{}
ss := secretservice.SecretsService{
Client: &secretservice.SecretsManagerClientWrapper{
Closer: mockClient.Close,
SecretLister: func(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) secretservice.Iterator {
return mockClient.ListSecrets(ctx, req, opts...)
},
},
}
tests := []struct {
name string
projectID string
secrets []*secretmanagerpb.Secret
want []secretservice.SecretInfo
wantErr bool
}{
{
name: "Retrieve secrets successfully",
projectID: "my-project",
secrets: []*secretmanagerpb.Secret{
{
Name: "projects/my-project/secrets/secret1",
Labels: map[string]string{"env": "test"},
CreateTime: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
},
want: []secretservice.SecretInfo{
{
Name: "projects/my-project/secrets/secret1",
ProjectID: "my-project",
CreationTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).String(),
Labels: map[string]string{"env": "test"},
},
},
wantErr: false,
},
{
name: "No secrets found",
projectID: "empty-project",
secrets: []*secretmanagerpb.Secret{},
want: []secretservice.SecretInfo{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient.secrets = tt.secrets
got, err := ss.Secrets(tt.projectID) // Adapt this call to match the actual method signature
if (err != nil) != tt.wantErr {
t.Errorf("[%s] Secrets() error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
}
if len(got) != len(tt.want) {
t.Errorf("[%s] Secrets() got length = %v, want length %v", tt.name, len(got), len(tt.want))
return
}
for i, g := range got {
w := tt.want[i]
// Compare Name, Labels, and CreationTime as string
if g.Name != w.Name || !reflect.DeepEqual(g.Labels, w.Labels) || g.CreationTime != w.CreationTime {
t.Errorf("Secrets() got = %v, want %v", g, w)
}
}
})
}
}
+1
View File
@@ -0,0 +1 @@
1.13.0-prerelease
+2
View File
@@ -21,6 +21,8 @@ const AZ_INVENTORY_MODULE_NAME = "inventory"
const AZ_VMS_MODULE_NAME = "vms"
const AZ_RBAC_MODULE_NAME = "rbac"
const AZ_STORAGE_MODULE_NAME = "storage"
const AZ_NSG_LINKS_MODULE_NAME = "nsg-links"
const AZ_NSG_RULES_MODULE_NAME = "nsg-rules"
// Microsoft endpoints
const AZ_RESOURCE_MANAGER_ENDPOINT = "https://management.azure.com/"
-17
View File
@@ -1,17 +0,0 @@
package globals
// Module names
// const GCP_WHOAMI_MODULE_NAME = "whoami"
const GCP_ARTIFACT_RESGISTRY_MODULE_NAME string = "artifact-registry"
const GCP_BIGQUERY_MODULE_NAME string = "bigquery"
const GCP_BUCKETS_MODULE_NAME string = "buckets"
const GCP_INSTANCES_MODULE_NAME string = "instances"
const GCP_IAM_MODULE_NAME string = "iam"
const GCP_SECRETS_MODULE_NAME string = "secrets"
const GCP_WHOAMI_MODULE_NAME string = "whoami"
// const GCP_INVENTORY_MODULE_NAME string = "inventory"
// const GCP_GCLOUD_REFRESH_TOKENS_DB_PATH = ".config/gcloud/credentials.db"
// const GCP_GCLOUD_ACCESS_TOKENS_DB_PATH = ".config/gcloud/access_tokens.db"
// const GCP_GCLOUD_DEFAULT_CONFIG_PATH = ".config/gcloud/configurations/config_default"
// const GCP_GCLOUD_APPLICATION_DEFAULT_PATH = ".config/gcloud/application_default_credentials.json"
+8 -1
View File
@@ -1,7 +1,14 @@
package globals
import (
_ "embed"
"strings"
)
const CLOUDFOX_USER_AGENT = "cloudfox"
const CLOUDFOX_LOG_FILE_DIR_NAME = ".cloudfox"
const CLOUDFOX_BASE_DIRECTORY = "cloudfox-output"
const LOOT_DIRECTORY_NAME = "loot"
const CLOUDFOX_VERSION = "1.13.5"
var CLOUDFOX_VERSION string = strings.TrimSpace(version)
//go:embed VERSION
var version string
+5 -47
View File
@@ -3,12 +3,6 @@ module github.com/BishopFox/cloudfox
go 1.21.2
require (
cloud.google.com/go/artifactregistry v1.14.6
cloud.google.com/go/bigquery v1.57.1
cloud.google.com/go/iam v1.1.5
cloud.google.com/go/resourcemanager v1.9.4
cloud.google.com/go/secretmanager v1.11.4
cloud.google.com/go/storage v1.35.1
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
@@ -16,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.1
github.com/aws/aws-sdk-go-v2 v1.24.0
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
@@ -58,10 +52,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.26.6
github.com/aws/smithy-go v1.19.0
github.com/bishopfox/awsservicemap v1.0.3
github.com/bishopfox/knownawsaccountslookup v0.0.0-20231228165844-c37ef8df33cb
github.com/dominikbraun/graph v0.23.0
github.com/fatih/color v1.16.0
github.com/googleapis/gax-go/v2 v2.12.0
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/patrickmn/go-cache v2.1.0+incompatible
@@ -69,45 +61,11 @@ require (
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0
golang.org/x/crypto v0.17.0
golang.org/x/oauth2 v0.15.0
google.golang.org/api v0.152.0
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17
google.golang.org/protobuf v1.31.0
)
require (
cloud.google.com/go v0.110.10 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/longrunning v0.5.4 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/apache/arrow/go/v12 v12.0.0 // indirect
github.com/apache/thrift v0.16.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/bishopfox/knownawsaccountslookup v0.0.0-20231228165844-c37ef8df33cb // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v2.0.8+incompatible // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/klauspost/asmfmt v1.3.2 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.9.1 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
@@ -127,8 +85,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.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.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/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
@@ -142,7 +100,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.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/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
+9 -202
View File
@@ -1,26 +1,3 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7EUsrQHG+/t8=
cloud.google.com/go/artifactregistry v1.14.6/go.mod h1:np9LSFotNWHcjnOgh8UVK0RFPCTUGbO0ve3384xyHfE=
cloud.google.com/go/bigquery v1.57.1 h1:FiULdbbzUxWD0Y4ZGPSVCDLvqRSyCIO6zKV7E2nf5uA=
cloud.google.com/go/bigquery v1.57.1/go.mod h1:iYzC0tGVWt1jqSzBHqCr3lrRn0u13E8e+AqowBsDgug=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datacatalog v1.18.3 h1:zmdxP6nOjN5Qb1rtu9h4kbEVwerQ6Oshf+t747QJUew=
cloud.google.com/go/datacatalog v1.18.3/go.mod h1:5FR6ZIF8RZrtml0VUao22FxhdjkoG+a0866rEnObryM=
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg=
cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
cloud.google.com/go/resourcemanager v1.9.4 h1:JwZ7Ggle54XQ/FVYSBrMLOQIKoIT/uer8mmNvNLK51k=
cloud.google.com/go/resourcemanager v1.9.4/go.mod h1:N1dhP9RFvo3lUfwtfLWVxfUWq8+KUQ+XLlHLH3BoFJ0=
cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k=
cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w=
cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w=
cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
@@ -30,13 +7,10 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1 h1:AMf7YbZOZIW5b66cXNHMWWT/zkjhz5+a+k/3x40EO7E=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1/go.mod h1:uwfk06ZBcvL/g4VHNjurPfVln9NMbsk2XIZxJ+hu81k=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
@@ -68,21 +42,12 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 h1:hVeq+yCyUi+MsoO/CU95yqCIcdzra5ovzk8Q2BBpV2M=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc=
github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg=
github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY=
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/aquasecurity/table v1.8.0 h1:9ntpSwrUfjrM6/YviArlx/ZBGd6ix8W+MtojQcM7tv0=
github.com/aquasecurity/table v1.8.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go-v2 v1.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 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/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=
@@ -91,10 +56,10 @@ github.com/aws/aws-sdk-go-v2/credentials v1.16.13 h1:WLABQ4Cp4vXtXfOWOS3MEZKr6AA
github.com/aws/aws-sdk-go-v2/credentials v1.16.13/go.mod h1:Qg6x82FXwW0sJHzYruxGiuApNo31UEtJvXVSZAXeWiw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58=
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.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.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/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/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/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=
@@ -163,8 +128,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucB
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.10 h1:h8uweImUHGgyNKrxIUwpPs6XiH0a6DJ17hSJvFLgPAo=
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.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/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/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=
@@ -215,9 +180,6 @@ github.com/bishopfox/awsservicemap v1.0.3 h1:0T+mJLwG+vQV9+o3dzwzxhWJWE40VpoCLWt
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
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=
@@ -225,72 +187,25 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY=
github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho=
github.com/go-openapi/strfmt v0.21.10 h1:JIsly3KXZB/Qf4UzvzJpg4OELH/0ASDQsyk//TTBDDk=
github.com/go-openapi/strfmt v0.21.10/go.mod h1:vNDMwbilnl7xKiO/Ve/8H8Bb2JIInBnH+lqiw6QWgis=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM=
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
@@ -299,17 +214,7 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
@@ -321,10 +226,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -334,18 +235,13 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@@ -362,26 +258,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -389,43 +275,17 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -444,7 +304,6 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -452,67 +311,15 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
google.golang.org/api v0.152.0 h1:t0r1vPnfMc260S2Ci+en7kfCZaLOPs5KI0sVV/6jZrY=
google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-119
View File
@@ -1,119 +0,0 @@
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
}
+224
View File
@@ -3,6 +3,7 @@ package internal
import (
"fmt"
"log"
"context"
"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute"
@@ -15,10 +16,146 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/BishopFox/cloudfox/globals"
"github.com/spf13/cobra"
)
type AzureClient struct {
AzVerbosity int
AzWrapTable bool
AzMergedTable bool
Version string
AzOutputFormat string
AzOutputDirectory string
AzTenants []*subscriptions.TenantIDDescription
AzSubscriptions []*subscriptions.Subscription
AzSubscriptionsAlt map[string][]*subscriptions.Subscription
AzRGs []*resources.Group
AzResources []*azure.Resource
Log *Logger
}
func (a *AzureClient) init (AzTenantRefs, AzSubscriptionRefs, AzRGRefs, AzResourceRefs []string){
availableSubscriptions := GetSubscriptions()
availableTenants := GetTenants()
// resource identifiers were submitted on the CLI, running modules on them only
if len(AzResourceRefs) > 0 {
a.Log.Info(nil, "Azure resource identifiers submitted, skipping submitted tenants and subscriptions")
// remove any other resource scope filter
a.AzTenants = nil
a.AzSubscriptions = nil
a.AzRGs = nil
var (
err error
resource azure.Resource
)
// check if the submitted resource can be reached with existing credentials
// this does not guarantees that the resource will be found since only the prefix of the resource ID
// is checked against the available ones
for _, azResourceRef := range AzResourceRefs {
resource, err = azure.ParseResourceID(azResourceRef)
if err != nil {
a.Log.Errorf(nil, "Invalid resource identifier : %s", azResourceRef)
continue
}
for _, subscription := range availableSubscriptions {
if resource.SubscriptionID == *subscription.SubscriptionID {
// add the resource to the final list of targets
a.AzResources = append(a.AzResources, &resource)
// also add the associated subscription since you cannot query a resource alone
a.AzSubscriptions = append(a.AzSubscriptions, &subscription)
goto FOUND_RESOURCE
}
}
a.Log.Errorf(nil, "No active credentials valid for resource %s, removing from target list", azResourceRef)
FOUND_RESOURCE:
}
} else if len(AzRGRefs) > 0 {
// resource groups were submitted on the CLI, running modules on them only
a.Log.Info(nil, "Azure subscriptions submitted, skipping submitted tenants")
a.AzTenants = nil
a.AzSubscriptions = nil
for _, AzRGRef := range AzRGRefs {
for _, subscription := range availableSubscriptions {
for _, rg := range GetResourceGroups(*subscription.SubscriptionID) {
if *rg.ID == AzRGRef {
a.AzRGs = append(a.AzRGs, &rg)
a.AzSubscriptions = append(a.AzSubscriptions, &subscription)
goto FOUND_RG
} else if *rg.Name == AzRGRef {
a.AzRGs = append(a.AzRGs, &rg)
a.AzSubscriptions = append(a.AzSubscriptions, &subscription)
goto FOUND_RG
}
}
}
a.Log.Warnf(nil, "Resource Group %s not accessible with active CLI credentials, removing from targets", AzRGRef)
FOUND_RG:
}
} else if len(AzSubscriptionRefs) > 0 {
// subscriptions were submitted on the CLI, running modules on them only
a.Log.Info(nil, "Azure subscriptions submitted, skipping submitted tenants")
// remove any other resource scope filter
a.AzTenants = nil
a.AzSubscriptionsAlt = make(map[string][]*subscriptions.Subscription, 0)
for _, AzSubscriptionRef := range AzSubscriptionRefs {
for _, subscription := range availableSubscriptions {
if (*subscription.SubscriptionID == AzSubscriptionRef) || (*subscription.DisplayName == AzSubscriptionRef) {
a.AzSubscriptions = append(a.AzSubscriptions, &subscription)
for _, AzTenant := range availableTenants {
if *subscription.TenantID == *AzTenant.TenantID {
a.AzSubscriptionsAlt[*AzTenant.DefaultDomain] = append(a.AzSubscriptionsAlt[*AzTenant.DefaultDomain], &subscription)
break
}
}
goto FOUND_SUB
}
}
a.Log.Warnf(nil, "Subscription %s not accessible with active CLI credentials, removing from targets", AzSubscriptionRef)
FOUND_SUB:
}
} else if len(AzTenantRefs) > 0 {
// tenants were submitted on the CLI, running modules on them only
for _, AzTenantRef := range AzTenantRefs {
for _, tenant := range availableTenants {
if *tenant.ID == AzTenantRef {
a.AzTenants = append(a.AzTenants, &tenant)
goto FOUND_TENANT
} else if *tenant.DefaultDomain == AzTenantRef {
a.AzTenants = append(a.AzTenants, &tenant)
goto FOUND_TENANT
}
}
a.Log.Warnf(nil, "Tenant %s not accessible with active CLI credentials, removing from targets", AzTenantRef)
FOUND_TENANT:
}
}
}
func NewAzureClient(AzVerbosity int, AzWrapTable, AzMergedTable bool, AzTenantRefs, AzSubscriptionRefs, AzRGRefs, AzResourceRefs []string, cmd *cobra.Command, AzOutputFormat, AzOutputDirectory string) *AzureClient {
client := new(AzureClient)
client.Log = NewLogger("azure")
client.Log.Announce(nil, "Searching existing credentials and validating submitted targets...")
client.Version = cmd.Root().Version
client.AzWrapTable = AzWrapTable
client.AzMergedTable = AzMergedTable
client.AzVerbosity = AzVerbosity
client.AzOutputFormat = AzOutputFormat
client.AzOutputDirectory = AzOutputDirectory
client.init(AzTenantRefs, AzSubscriptionRefs, AzRGRefs, AzResourceRefs)
return client
}
func getAuthorizer(endpoint string) (autorest.Authorizer, error) {
auth, err := auth.NewAuthorizerFromCLIWithResource(endpoint)
if err != nil {
@@ -126,6 +263,39 @@ func GetNICClient(subscriptionID string) network.InterfacesClient {
return client
}
func GetNSGClient(subscriptionID string) network.SecurityGroupsClient {
client := network.NewSecurityGroupsClient(subscriptionID)
authorizer, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
if err != nil {
log.Fatalf("failed to get nsg client: %s", err)
}
client.Authorizer = authorizer
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
return client
}
func GetSubnetsClient(subscriptionID string) network.SubnetsClient {
client := network.NewSubnetsClient(subscriptionID)
authorizer, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
if err != nil {
log.Fatalf("failed to get subnets client: %s", err)
}
client.Authorizer = authorizer
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
return client
}
func GetVNETsClient(subscriptionID string) network.VirtualNetworksClient {
client := network.NewVirtualNetworksClient(subscriptionID)
authorizer, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
if err != nil {
log.Fatalf("failed to get subnets client: %s", err)
}
client.Authorizer = authorizer
client.AddToUserAgent(globals.CLOUDFOX_USER_AGENT)
return client
}
func GetPublicIPClient(subscriptionID string) network.PublicIPAddressesClient {
client := network.NewPublicIPAddressesClient(subscriptionID)
authorizer, err := getAuthorizer(globals.AZ_RESOURCE_MANAGER_ENDPOINT)
@@ -171,3 +341,57 @@ func GetARMresourcesClient(tenantID, subscriptionID string) *armresources.Client
}
return client
}
func GetIPAddressesFromInterface(iface *network.Interface) []string{
var ipAddresses []string
for _, ipConfiguration := range *iface.InterfacePropertiesFormat.IPConfigurations {
if ipConfiguration.InterfaceIPConfigurationPropertiesFormat.PrivateIPAddress != nil {
ipAddresses = append(ipAddresses, *ipConfiguration.InterfaceIPConfigurationPropertiesFormat.PrivateIPAddress)
}
if ipConfiguration.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress != nil {
if ipConfiguration.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress.PublicIPAddressPropertiesFormat != nil {
if ipConfiguration.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress.PublicIPAddressPropertiesFormat.IPAddress != nil {
ipAddresses = append(ipAddresses, *ipConfiguration.InterfaceIPConfigurationPropertiesFormat.PublicIPAddress.PublicIPAddressPropertiesFormat.IPAddress)
}
}
}
}
return ipAddresses
}
func GetSubscriptions() []subscriptions.Subscription {
var results []subscriptions.Subscription
subsClient := GetSubscriptionsClient()
for page, err := subsClient.List(context.TODO()); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatal("could not get subscriptions for active session")
}
results = append(results, page.Values()...)
}
return results
}
func GetTenants() []subscriptions.TenantIDDescription {
tenantsClient := GetTenantsClient()
var results []subscriptions.TenantIDDescription
for page, err := tenantsClient.List(context.TODO()); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatal("could not get tenants for active session")
}
results = append(results, page.Values()...)
}
return results
}
func GetResourceGroups(subscriptionID string) []resources.Group {
var results []resources.Group
rgClient := GetResourceGroupsClient(subscriptionID)
for page, err := rgClient.List(context.TODO(), "", nil); page.NotDone(); err = page.Next() {
if err != nil {
log.Fatalf("error reading resource groups for subscription %s", subscriptionID)
}
results = append(results, page.Values()...)
}
return results
}
+84 -30
View File
@@ -1,17 +1,17 @@
package internal
import (
"fmt"
"log"
"fmt"
"os"
"os/user"
"path/filepath"
"github.com/BishopFox/cloudfox/globals"
"github.com/aws/smithy-go/ptr"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/text"
"github.com/kyokomi/emoji"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
)
@@ -37,51 +37,105 @@ func GetLogDirPath() *string {
type Logger struct {
version string
txtLog *logrus.Logger
module string
txtLog *logrus.Logger
Cyan func(...interface{}) string
Red func(...interface{}) string
Green func(...interface{}) string
Yellow func(...interface{}) string
}
func NewLogger() Logger {
func NewLogger(module string) *Logger {
lootLogger := logrus.New()
logDirPath := GetLogDirPath()
logFile, err := os.OpenFile(filepath.Join(*logDirPath, "cloudfox.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic("Could not initiate logger")
}
lootLogger.Out = logFile
lootLogger.SetLevel(logrus.InfoLevel)
var logger = Logger{
version: globals.CLOUDFOX_VERSION,
txtLog: TxtLog,
txtLog: lootLogger,
module: module,
Cyan: color.New(color.FgCyan).SprintFunc(),
Red: color.New(color.FgRed).SprintFunc(),
Green:color.New(color.FgGreen).SprintFunc(),
Yellow: color.New(color.FgYellow).SprintFunc(),
}
return logger
return &logger
}
func (l *Logger) Info(text string) {
l.InfoM(text, "config")
func (l *Logger) Print(categories []string, color func(...interface{}) string, text string) {
formatString := text
formatArgs := []any{}
l.Printf(categories, color, formatString, formatArgs...)
}
func (l *Logger) InfoM(text string, module string) {
var cyan = color.New(color.FgCyan).SprintFunc()
fmt.Printf("[%s][%s] %s\n", cyan(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), cyan(module), text)
func (l *Logger) Printf(categories []string, color func(...interface{}) string, format string, params ...any) {
formatString := ""
formatArgs := []any{}
blocks := []string{}
blocks = append(blocks, categories...)
for _, block := range blocks {
formatString += "[%s]"
formatArgs = append(formatArgs, color(block))
}
for _, param := range params {
formatArgs = append(formatArgs, color(param))
}
formatString += " " + format + "\n"
fmt.Printf(formatString, formatArgs...)
}
func (l *Logger) Success(text string) {
l.SuccessM(text, "config")
}
func (l *Logger) SuccessM(text string, module string) {
var green = color.New(color.FgGreen).SprintFunc()
fmt.Printf("[%s][%s] %s\n", green(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), green(module), text)
func (l *Logger) Announce(categories []string, text string) {
l.Print(append([]string{emoji.Sprintf(":fox:cloudfox v%s :fox:", l.version), l.module}, categories...), l.Cyan, text)
}
func (l *Logger) Error(text string) {
l.ErrorM(text, "config")
func (l *Logger) Announcef(categories []string, format string, params ...any) {
l.Printf(append([]string{emoji.Sprintf(":fox:cloudfox v%s :fox:", l.version), l.module}, categories...), l.Cyan, format, params...)
}
func (l *Logger) ErrorM(text string, module string) {
var red = color.New(color.FgRed).SprintFunc()
fmt.Printf("[%s][%s] %s\n", red(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), red(module), text)
l.txtLog.Printf("[%s] %s", module, text)
func (l *Logger) Info(categories []string, text string) {
l.Print(append([]string{l.module}, categories...), l.Cyan, text)
}
func (l *Logger) Fatal(text string) {
l.FatalM(text, "config")
func (l *Logger) Infof(categories []string, format string, params ...any) {
l.Printf(append([]string{l.module}, categories...), l.Cyan, format, params...)
}
func (l *Logger) FatalM(text string, module string) {
var red = color.New(color.FgRed).SprintFunc()
l.txtLog.Printf("[%s] %s", module, text)
fmt.Printf("[%s][%s] %s\n", red(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), red(module), text)
os.Exit(1)
func (l *Logger) Success(categories []string, text string) {
l.Print(append([]string{l.module}, categories...), l.Green, text)
}
func (l *Logger) Successf(categories []string, format string, params ...any) {
l.Printf(append([]string{l.module}, categories...), l.Green, format, params...)
}
func (l *Logger) Warn(categories []string, text string) {
l.Print(append([]string{l.module}, categories...), l.Yellow, text)
}
func (l *Logger) Warnf(categories []string, format string, params ...any) {
l.Printf(append([]string{l.module}, categories...), l.Yellow, format, params...)
}
func (l *Logger) Error(categories []string, text string) {
l.Print(append([]string{l.module}, categories...), l.Red, text)
}
func (l *Logger) Errorf(categories []string, format string, params ...any) {
l.Printf(append([]string{l.module}, categories...), l.Red, format, params...)
}
func (l *Logger) Fatal(categories []string, text string) {
l.Print(append([]string{emoji.Sprintf(":fox:cloudfox v%s :fox:", l.version), l.module}, categories...), l.Red, text)
panic(nil)
}
func (l *Logger) Fatalf(categories []string, format string, params ...any) {
l.Printf(append([]string{emoji.Sprintf(":fox:cloudfox v%s :fox:", l.version), l.module}, categories...), l.Red, format, params...)
panic(nil)
}
+2 -47
View File
@@ -7,7 +7,6 @@ import (
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
@@ -60,49 +59,6 @@ type LootFile struct {
Contents string
}
// TODO support datastructures that enable brief or wide format
type CloudfoxOutput interface {
TableFiles() []TableFile
LootFiles() []LootFile
}
// HandleOutput dynamically handles the output based on the provided arguments.
// TODO support brief of wide
func HandleOutput(
cloudProvider string,
format string,
outputDirectory string,
verbosity int,
wrap bool,
baseCloudfoxModule string,
principal string,
resultsIdentifier string,
dataToOutput CloudfoxOutput,
) error {
// Update OutputClient fields based on arguments
outDirectoryPath := filepath.Join(outputDirectory, "cloudfox-output", cloudProvider, fmt.Sprintf("%s-%s", principal, resultsIdentifier), baseCloudfoxModule)
tables := dataToOutput.TableFiles()
lootFiles := dataToOutput.LootFiles()
outputClient := OutputClient{
Verbosity: verbosity,
CallingModule: baseCloudfoxModule,
Table: TableClient{
Wrap: wrap,
DirectoryName: outDirectoryPath,
TableFiles: tables,
},
Loot: LootClient{
DirectoryName: outDirectoryPath,
LootFiles: lootFiles,
},
}
// Handle output based on the verbosity level
outputClient.WriteFullOutput(tables, lootFiles)
return nil
}
func removeColorCodes(input string) string {
// Regular expression to match ANSI color codes
ansiRegExp := regexp.MustCompile(`\x1b\[[0-9;]*m`)
@@ -141,7 +97,7 @@ func removeColorCodesFromNestedSlice(input [][]string) [][]string {
}
func (o *OutputClient) WriteFullOutput(tables []TableFile, lootFiles []LootFile) {
logger := NewLogger()
switch o.Verbosity {
case 2:
o.Table.printTablesToScreen(tables)
@@ -171,8 +127,7 @@ func (o *OutputClient) WriteFullOutput(tables []TableFile, lootFiles []LootFile)
}
for _, path := range outputPaths {
logger.InfoM(fmt.Sprintf("Output written to %s", path), o.CallingModule)
// fmt.Printf("[%s][%s] Output written to %s\n", cyan(o.CallingModule), cyan(o.PrefixIdentifier), path)
fmt.Printf("[%s][%s] Output written to %s\n", cyan(o.CallingModule), cyan(o.PrefixIdentifier), path)
}
}
+3 -2
View File
@@ -4,17 +4,18 @@ import (
"os"
"github.com/BishopFox/cloudfox/cli"
"github.com/BishopFox/cloudfox/globals"
"github.com/spf13/cobra"
)
var (
rootCmd = &cobra.Command{
Use: os.Args[0],
Version: "1.14.0-prerelease",
Version: globals.CLOUDFOX_VERSION,
}
)
func main() {
rootCmd.AddCommand(cli.AWSCommands, cli.AzCommands, cli.GCPCommands)
rootCmd.AddCommand(cli.AWSCommands, cli.AzCommands)
rootCmd.Execute()
}