removed attackpath and orgcache flags and enable by default

This commit is contained in:
jbarciabf committed 2026-02-16 15:17:04 -05:00
1 parent cb8be678e7
commit 7a18090609
23 files changed
+384 -471

No files matched your search

+9 -8
View File
@@ -301,21 +301,22 @@ For detailed documentation on each GCP command, see the [GCP Commands Wiki](http
CloudFox GCP uses a **unified attack path analysis** system that combines privilege escalation, lateral movement, and data exfiltration analysis. The three attack path modules share a common backend (`attackpathService`) that analyzes IAM policies across all 4 hierarchy levels: Organization → Folder → Project → Resource.
### `--attack-paths` Global Flag
### Attack Path Column
When running compute/service modules, add `--attack-paths` to see attack path capabilities for service accounts:
CloudFox automatically loads FoxMapper graph data when available and shows attack path capabilities in module output. Run foxmapper first to populate the Attack Paths column:
```bash
# Run instances module with attack path analysis
cloudfox gcp instances -p my-project --attack-paths
# First, generate the attack path graph
foxmapper gcp graph create -p my-project
# Run service accounts with attack paths
cloudfox gcp serviceaccounts --all-projects --attack-paths
# Then run cloudfox modules - Attack Paths column will be populated automatically
cloudfox gcp instances -p my-project
cloudfox gcp serviceaccounts --all-projects
```
This adds an **"Attack Paths"** column showing: `Yes (P:3 E:2 L:1)` where P=Privesc, E=Exfil, L=Lateral counts.
The **"Attack Paths"** column shows: `Yes (P:3 E:2 L:1)` where P=Privesc, E=Exfil, L=Lateral counts. If foxmapper hasn't been run, the column shows "run foxmapper".
**Modules supporting `--attack-paths`**: instances, serviceaccounts, functions, cloudrun, gke, composer, dataproc, dataflow, notebooks, cloudbuild, scheduler, appengine
**Modules with Attack Paths column**: instances, serviceaccounts, functions, cloudrun, gke, composer, dataproc, dataflow, notebooks, cloudbuild, scheduler, appengine, service-agents
| Provider | Command Name | Description |
| - | - | - |
+15 -24
View File
@@ -32,12 +32,6 @@ var (
GCPWrapTable bool
GCPFlatOutput bool
// Attack path analysis flag
GCPAttackPaths bool
// Organization cache flag - enumerates all orgs/folders/projects for cross-project analysis
GCPOrgCache bool
// Refresh cache flag - force re-enumeration even if cache exists
GCPRefreshCache bool
@@ -125,11 +119,9 @@ var (
// Get account for cache operations
account, _ := ctx.Value("account").(string)
// If --attack-paths flag is set, try to load FoxMapper data
// Always try to load FoxMapper data for attack path analysis
// This allows individual modules to show the Attack Paths column
if GCPAttackPaths && len(GCPProjectIDs) > 0 {
GCPLogger.InfoM("Looking for FoxMapper graph data...", "gcp")
if len(GCPProjectIDs) > 0 {
// Get org ID from hierarchy if available (GCPOrganization flag may be empty)
orgID := GCPOrganization
if orgID == "" {
@@ -144,21 +136,22 @@ var (
if foxMapperCache != nil && foxMapperCache.IsPopulated() {
ctx = gcpinternal.SetFoxMapperCacheInContext(ctx, foxMapperCache)
totalNodes, adminNodes, nodesWithPrivesc := foxMapperCache.GetStats()
GCPLogger.SuccessM(fmt.Sprintf("FoxMapper data loaded: %d principals, %d admins, %d with privesc - modules will show Attack Paths column",
totalNodes, adminNodes, nodesWithPrivesc), "gcp")
} else {
GCPLogger.InfoM("No FoxMapper data found. Run 'foxmapper gcp graph create' to generate graph data for attack path analysis.", "gcp")
ageDays := foxMapperCache.GetDataAgeDays()
if ageDays >= 7 {
GCPLogger.WarnM(fmt.Sprintf("FoxMapper data is %d days old - consider running 'foxmapper gcp graph create' to refresh",
ageDays), "gcp")
}
GCPLogger.SuccessM(fmt.Sprintf("FoxMapper data loaded: %d principals, %d admins, %d with privesc (data age: %d days)",
totalNodes, adminNodes, nodesWithPrivesc, ageDays), "gcp")
}
}
// If --org-cache flag is set, load or enumerate all orgs/folders/projects
// This is useful for cross-project analysis modules
if GCPOrgCache {
GCPLogger.InfoM("Loading/enumerating organization data...", "gcp")
orgCache := loadOrPopulateOrgCache(account, GCPRefreshCache)
if orgCache != nil && orgCache.IsPopulated() {
ctx = gcpinternal.SetOrgCacheInContext(ctx, orgCache)
}
// Always try to load org cache for cross-project analysis
// Cache auto-refreshes after 24 hours
orgCache := loadOrPopulateOrgCache(account, GCPRefreshCache)
if orgCache != nil && orgCache.IsPopulated() {
ctx = gcpinternal.SetOrgCacheInContext(ctx, orgCache)
}
cmd.SetContext(ctx)
@@ -487,8 +480,6 @@ func init() {
// 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)")
GCPCommands.PersistentFlags().BoolVar(&GCPFlatOutput, "flat-output", false, "Use legacy flat output structure instead of hierarchical per-project directories")
GCPCommands.PersistentFlags().BoolVar(&GCPAttackPaths, "attack-paths", false, "Run attack path analysis (privesc/exfil/lateral) and add Attack Paths column to module output")
GCPCommands.PersistentFlags().BoolVar(&GCPOrgCache, "org-cache", false, "Enumerate all accessible orgs/folders/projects and cache for cross-project analysis")
GCPCommands.PersistentFlags().BoolVar(&GCPRefreshCache, "refresh-cache", false, "Force re-enumeration of cached data (cache auto-expires after 24 hours)")
// Available commands
+1 -1
View File
@@ -534,7 +534,7 @@ func (m *AppEngineModule) buildTablesForProject(projectID string, apps []AppEngi
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if ver.ServiceAccount != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, ver.ServiceAccount)
+85 -47
View File
@@ -14,7 +14,7 @@ import (
"github.com/spf13/cobra"
"google.golang.org/api/compute/v1"
"google.golang.org/api/sqladmin/v1beta4"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
)
// Module name constant
@@ -62,18 +62,20 @@ type IAMBinding struct {
}
type ComputeSnapshot struct {
ProjectID string
Name string
SourceDisk string
Status string
DiskSizeGB int64
StorageBytes int64
CreationTime string
StorageLocats []string
AutoCreated bool
SnapshotType string
IAMBindings []IAMBinding
PublicAccess bool
ProjectID string
Name string
SourceDisk string
Status string
DiskSizeGB int64
StorageBytes int64
CreationTime string
StorageLocats []string
AutoCreated bool
SnapshotType string
IAMBindings []IAMBinding
PublicAccess bool
EncryptionType string
KMSKeyName string
}
// ------------------------------
@@ -82,8 +84,8 @@ type ComputeSnapshot struct {
type BackupInventoryModule struct {
gcpinternal.BaseGCPModule
ProjectResources map[string][]BackupResource // projectID -> resources
ProjectSnapshots map[string][]ComputeSnapshot // projectID -> snapshots
ProjectResources map[string][]BackupResource // projectID -> resources
ProjectSnapshots map[string][]ComputeSnapshot // projectID -> snapshots
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
@@ -291,17 +293,31 @@ func (m *BackupInventoryModule) enumerateSnapshots(ctx context.Context, projectI
req := computeService.Snapshots.List(projectID)
err := req.Pages(ctx, func(page *compute.SnapshotList) error {
for _, snapshot := range page.Items {
// Determine encryption type and KMS key name
encryptionType := "Google-managed"
kmsKeyName := ""
if snapshot.SnapshotEncryptionKey != nil {
if snapshot.SnapshotEncryptionKey.KmsKeyName != "" {
encryptionType = "CMEK"
kmsKeyName = snapshot.SnapshotEncryptionKey.KmsKeyName
} else if snapshot.SnapshotEncryptionKey.RawKey != "" || snapshot.SnapshotEncryptionKey.Sha256 != "" {
encryptionType = "CSEK"
}
}
snap := ComputeSnapshot{
ProjectID: projectID,
Name: snapshot.Name,
SourceDisk: snapshot.SourceDisk,
Status: snapshot.Status,
DiskSizeGB: snapshot.DiskSizeGb,
StorageBytes: snapshot.StorageBytes,
CreationTime: snapshot.CreationTimestamp,
StorageLocats: snapshot.StorageLocations,
AutoCreated: snapshot.AutoCreated,
SnapshotType: snapshot.SnapshotType,
ProjectID: projectID,
Name: snapshot.Name,
SourceDisk: snapshot.SourceDisk,
Status: snapshot.Status,
DiskSizeGB: snapshot.DiskSizeGb,
StorageBytes: snapshot.StorageBytes,
CreationTime: snapshot.CreationTimestamp,
StorageLocats: snapshot.StorageLocations,
AutoCreated: snapshot.AutoCreated,
SnapshotType: snapshot.SnapshotType,
EncryptionType: encryptionType,
KMSKeyName: kmsKeyName,
}
// Get IAM policy for this snapshot
@@ -324,6 +340,34 @@ func (m *BackupInventoryModule) enumerateSnapshots(ctx context.Context, projectI
m.mu.Lock()
m.ProjectSnapshots[projectID] = append(m.ProjectSnapshots[projectID], snap)
m.disksWithBackups[snapshot.SourceDisk] = true
// Add post-exploit commands for snapshots
if m.LootMap[projectID] != nil {
if lootFile := m.LootMap[projectID]["backup-inventory-commands"]; lootFile != nil {
// Determine a zone from storage locations or use a default
zone := "us-central1-a"
if len(snapshot.StorageLocations) > 0 {
zone = snapshot.StorageLocations[0] + "-a"
}
lootFile.Contents += fmt.Sprintf(
"### Snapshot: %s (Source: %s, Size: %dGB)\n"+
"# Create a disk from this snapshot\n"+
"gcloud compute disks create disk-from-%s \\\n"+
" --project=%s \\\n"+
" --zone=%s \\\n"+
" --source-snapshot=%s\n\n"+
"# Create an instance using a disk from this snapshot\n"+
"gcloud compute instances create instance-from-%s \\\n"+
" --project=%s \\\n"+
" --zone=%s \\\n"+
" --disk=name=disk-from-%s,boot=yes\n\n",
snapshot.Name, m.extractDiskName(snapshot.SourceDisk), snapshot.DiskSizeGb,
snapshot.Name, projectID, zone, snapshot.Name,
snapshot.Name, projectID, zone, snapshot.Name,
)
}
}
m.mu.Unlock()
}
return nil
@@ -504,18 +548,7 @@ func (m *BackupInventoryModule) identifyUnprotectedResources() {
Contents: "# Backup Inventory Commands\n# Generated by CloudFox\n\n",
}
}
if lootFile := m.LootMap[info.ProjectID]["backup-inventory-commands"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# Unprotected disk: %s (%s) - %dGB\n"+
"gcloud compute resource-policies create snapshot-schedule %s-backup \\\n"+
" --project=%s \\\n"+
" --region=%s \\\n"+
" --max-retention-days=30 \\\n"+
" --daily-schedule\n\n",
info.Name, info.ProjectID, info.SizeGB,
info.Name, info.ProjectID, m.extractRegionFromZone(info.Zone),
)
}
// No loot commands for unprotected disks - these are informational only
}
}
@@ -541,15 +574,7 @@ func (m *BackupInventoryModule) identifyUnprotectedResources() {
Contents: "# Backup Inventory Commands\n# Generated by CloudFox\n\n",
}
}
if lootFile := m.LootMap[info.ProjectID]["backup-inventory-commands"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# Unprotected SQL instance: %s\n"+
"gcloud sql instances patch %s \\\n"+
" --backup-start-time=02:00 \\\n"+
" --enable-bin-log\n\n",
instanceName, instanceName,
)
}
// No loot commands for unprotected SQL instances - these are informational only
}
}
}
@@ -589,7 +614,6 @@ func (m *BackupInventoryModule) extractRegionFromZone(zone string) string {
return zone
}
// ------------------------------
// Output Generation
// ------------------------------
@@ -631,6 +655,7 @@ func (m *BackupInventoryModule) getSnapshotsHeader() []string {
"Type",
"Auto Created",
"Locations",
"Encryption",
"IAM Binding Role",
"IAM Binding Principal",
"Public",
@@ -698,6 +723,17 @@ func (m *BackupInventoryModule) snapshotsToTableBody(snapshots []ComputeSnapshot
publicAccess = "Yes"
}
// Format encryption - show KMS key name if CMEK
encryptionDisplay := s.EncryptionType
if s.EncryptionType == "CMEK" && s.KMSKeyName != "" {
// Extract just the key name from the full path for display
// Format: projects/PROJECT/locations/LOCATION/keyRings/RING/cryptoKeys/KEY
keyParts := strings.Split(s.KMSKeyName, "/")
if len(keyParts) >= 2 {
encryptionDisplay = fmt.Sprintf("CMEK (%s)", keyParts[len(keyParts)-1])
}
}
// If no IAM bindings, still show the snapshot
if len(s.IAMBindings) == 0 {
body = append(body, []string{
@@ -710,6 +746,7 @@ func (m *BackupInventoryModule) snapshotsToTableBody(snapshots []ComputeSnapshot
s.SnapshotType,
autoCreatedStr,
strings.Join(s.StorageLocats, ","),
encryptionDisplay,
"-",
"-",
publicAccess,
@@ -728,6 +765,7 @@ func (m *BackupInventoryModule) snapshotsToTableBody(snapshots []ComputeSnapshot
s.SnapshotType,
autoCreatedStr,
strings.Join(s.StorageLocats, ","),
encryptionDisplay,
binding.Role,
member,
publicAccess,
+18 -14
View File
@@ -14,10 +14,10 @@ import (
)
var (
bucketEnumMaxObjects int
bucketEnumAllObjects bool
bucketEnumNoLimit bool
maxObjectsWasSet bool // tracks if --max-objects was explicitly set
bucketEnumMaxObjects int
bucketEnumAllObjects bool
bucketEnumNoLimit bool
maxObjectsWasSet bool // tracks if --max-objects was explicitly set
)
var GCPBucketEnumCommand = &cobra.Command{
@@ -42,16 +42,20 @@ File categories detected:
- Source: Git repositories
- Cloud: Cloud Functions source, build artifacts
Use --all-objects to enumerate ALL bucket contents (not just sensitive files).
WARNING: Full enumeration may take a long time for buckets with many objects.
Use --max-objects to limit the scan, or --no-limit for unlimited.`,
Flags:
--all-objects Report ALL bucket objects (not just sensitive files)
--no-limit Remove the 1000 object-per-bucket scan limit
--max-objects Set a custom object-per-bucket scan limit
By default, only sensitive files are reported with a 1000 object scan limit.
WARNING: --all-objects and --no-limit may take a long time for large buckets.`,
Run: runGCPBucketEnumCommand,
}
func init() {
GCPBucketEnumCommand.Flags().IntVar(&bucketEnumMaxObjects, "max-objects", 1000, "Maximum objects to scan per bucket (default 1000)")
GCPBucketEnumCommand.Flags().BoolVar(&bucketEnumAllObjects, "all-objects", false, "Enumerate ALL bucket contents, not just sensitive files (implies --no-limit unless --max-objects is set)")
GCPBucketEnumCommand.Flags().BoolVar(&bucketEnumNoLimit, "no-limit", false, "Remove the object limit (enumerate all objects in each bucket)")
GCPBucketEnumCommand.Flags().IntVar(&bucketEnumMaxObjects, "max-objects", 1000, "Maximum objects to scan per bucket")
GCPBucketEnumCommand.Flags().BoolVar(&bucketEnumAllObjects, "all-objects", false, "Report ALL objects, not just sensitive files (implies --no-limit unless --max-objects is set)")
GCPBucketEnumCommand.Flags().BoolVar(&bucketEnumNoLimit, "no-limit", false, "Remove the 1000 object-per-bucket scan limit (still only reports sensitive files)")
}
type BucketEnumModule struct {
@@ -290,14 +294,14 @@ func (m *BucketEnumModule) addFileToLoot(projectID string, file bucketenumservic
localDir := fmt.Sprintf("bucket/%s/%s", file.BucketName, getObjectDir(file.ObjectName))
localCpCmd := fmt.Sprintf("gsutil cp gs://%s/%s %s", file.BucketName, file.ObjectName, localDir)
// All files go to the general commands file
// All files go to the general commands file (without risk ranking)
if lootFile := m.LootMap[projectID]["bucket-enum-commands"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# [%s] %s - gs://%s/%s\n"+
"# Category: %s, Size: %d bytes\n"+
"# %s - gs://%s/%s\n"+
"# %s, Size: %d bytes\n"+
"mkdir -p %s\n"+
"%s\n\n",
file.RiskLevel, file.Category,
file.Category,
file.BucketName, file.ObjectName,
file.Description, file.Size,
localDir,
+2 -118
View File
@@ -49,7 +49,6 @@ type CertManagerModule struct {
ProjectCertificates map[string][]certmanagerservice.Certificate // projectID -> certificates
ProjectSSLCertificates map[string][]certmanagerservice.SSLCertificate // projectID -> SSL certs
ProjectCertMaps map[string][]certmanagerservice.CertificateMap // projectID -> cert maps
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
}
@@ -78,7 +77,6 @@ func runGCPCertManagerCommand(cmd *cobra.Command, args []string) {
ProjectCertificates: make(map[string][]certmanagerservice.Certificate),
ProjectSSLCertificates: make(map[string][]certmanagerservice.SSLCertificate),
ProjectCertMaps: make(map[string][]certmanagerservice.CertificateMap),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
@@ -166,17 +164,6 @@ func (m *CertManagerModule) processProject(ctx context.Context, projectID string
svc := certmanagerservice.New()
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["certmanager-details"] = &internal.LootFile{
Name: "certmanager-details",
Contents: "# Certificate Manager Details\n# Generated by CloudFox\n\n",
}
}
m.mu.Unlock()
// Get Certificate Manager certs
certs, err := svc.GetCertificates(projectID)
if err != nil {
@@ -205,93 +192,9 @@ func (m *CertManagerModule) processProject(ctx context.Context, projectID string
m.ProjectCertificates[projectID] = certs
m.ProjectSSLCertificates[projectID] = sslCerts
m.ProjectCertMaps[projectID] = certMaps
for _, cert := range certs {
m.addCertToLoot(projectID, cert)
}
for _, cert := range sslCerts {
m.addSSLCertToLoot(projectID, cert)
}
m.mu.Unlock()
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *CertManagerModule) addCertToLoot(projectID string, cert certmanagerservice.Certificate) {
lootFile := m.LootMap[projectID]["certmanager-details"]
if lootFile == nil {
return
}
// Build flags for special attributes
var flags []string
if cert.Wildcard {
flags = append(flags, "WILDCARD")
}
if cert.Expired {
flags = append(flags, "EXPIRED")
} else if cert.DaysUntilExpiry <= 30 {
flags = append(flags, "EXPIRING")
}
if cert.SelfManaged {
flags = append(flags, "SELF-MANAGED")
}
flagStr := ""
if len(flags) > 0 {
flagStr = " [" + strings.Join(flags, "] [") + "]"
}
lootFile.Contents += fmt.Sprintf(
"# %s%s\n"+
"Project: %s | Location: %s\n"+
"Type: %s | State: %s\n"+
"Domains: %s\n"+
"Expires: %s (%d days)\n\n",
cert.Name, flagStr,
cert.ProjectID, cert.Location,
cert.Type, cert.State,
strings.Join(cert.Domains, ", "),
cert.ExpireTime, cert.DaysUntilExpiry,
)
}
func (m *CertManagerModule) addSSLCertToLoot(projectID string, cert certmanagerservice.SSLCertificate) {
lootFile := m.LootMap[projectID]["certmanager-details"]
if lootFile == nil {
return
}
// Build flags for special attributes
var flags []string
if cert.Wildcard {
flags = append(flags, "WILDCARD")
}
if cert.Expired {
flags = append(flags, "EXPIRED")
} else if cert.DaysUntilExpiry <= 30 {
flags = append(flags, "EXPIRING")
}
if cert.SelfManaged {
flags = append(flags, "SELF-MANAGED")
}
flagStr := ""
if len(flags) > 0 {
flagStr = " [" + strings.Join(flags, "] [") + "]"
}
lootFile.Contents += fmt.Sprintf(
"# %s (SSL Certificate)%s\n"+
"Project: %s | Type: %s\n"+
"Domains: %s\n"+
"Expires: %s (%d days)\n\n",
cert.Name, flagStr,
cert.ProjectID, cert.Type,
strings.Join(cert.Domains, ", "),
cert.ExpireTime, cert.DaysUntilExpiry,
)
}
// ------------------------------
// Output Generation
// ------------------------------
@@ -429,17 +332,7 @@ func (m *CertManagerModule) writeHierarchicalOutput(ctx context.Context, logger
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = CertManagerOutput{Table: tableFiles, Loot: lootFiles}
outputData.ProjectLevelData[projectID] = CertManagerOutput{Table: tableFiles}
}
pathBuilder := m.BuildPathBuilder()
@@ -473,16 +366,7 @@ func (m *CertManagerModule) writeFlatOutput(ctx context.Context, logger internal
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := CertManagerOutput{Table: tables, Loot: lootFiles}
output := CertManagerOutput{Table: tables}
scopeNames := make([]string, len(m.ProjectIDs))
for i, projectID := range m.ProjectIDs {
+80 -134
View File
@@ -43,10 +43,9 @@ What this module finds:
type CloudArmorModule struct {
gcpinternal.BaseGCPModule
ProjectPolicies map[string][]cloudarmorservice.SecurityPolicy // projectID -> policies
UnprotectedLBs map[string][]string // projectID -> LB names
LootMap map[string]map[string]*internal.LootFile // projectID -> loot files
mu sync.Mutex
ProjectPolicies map[string][]cloudarmorservice.SecurityPolicy // projectID -> policies
UnprotectedLBs map[string][]string // projectID -> LB names
mu sync.Mutex
}
// ------------------------------
@@ -73,7 +72,6 @@ func runGCPCloudArmorCommand(cmd *cobra.Command, args []string) {
BaseGCPModule: gcpinternal.NewBaseGCPModule(cmdCtx),
ProjectPolicies: make(map[string][]cloudarmorservice.SecurityPolicy),
UnprotectedLBs: make(map[string][]string),
LootMap: make(map[string]map[string]*internal.LootFile),
}
module.Execute(cmdCtx.Ctx, cmdCtx.Logger)
}
@@ -130,17 +128,6 @@ func (m *CloudArmorModule) processProject(ctx context.Context, projectID string,
logger.InfoM(fmt.Sprintf("Checking Cloud Armor in project: %s", projectID), globals.GCP_CLOUDARMOR_MODULE_NAME)
}
m.mu.Lock()
// Initialize loot for this project
if m.LootMap[projectID] == nil {
m.LootMap[projectID] = make(map[string]*internal.LootFile)
m.LootMap[projectID]["cloudarmor-details"] = &internal.LootFile{
Name: "cloudarmor-details",
Contents: "# Cloud Armor Details\n# Generated by CloudFox\n\n",
}
}
m.mu.Unlock()
svc := cloudarmorservice.New()
// Get security policies
@@ -164,103 +151,9 @@ func (m *CloudArmorModule) processProject(ctx context.Context, projectID string,
if len(unprotectedLBs) > 0 {
m.UnprotectedLBs[projectID] = unprotectedLBs
}
for _, policy := range policies {
m.addPolicyToLoot(projectID, policy)
}
for _, lb := range unprotectedLBs {
m.addUnprotectedLBToLoot(projectID, lb)
}
m.mu.Unlock()
}
// ------------------------------
// Loot File Management
// ------------------------------
func (m *CloudArmorModule) addPolicyToLoot(projectID string, policy cloudarmorservice.SecurityPolicy) {
lootFile := m.LootMap[projectID]["cloudarmor-details"]
if lootFile == nil {
return
}
// Build flags for special attributes
var flags []string
if len(policy.Weaknesses) > 0 {
flags = append(flags, "HAS WEAKNESSES")
}
flagStr := ""
if len(flags) > 0 {
flagStr = " [" + strings.Join(flags, "] [") + "]"
}
adaptive := "No"
if policy.AdaptiveProtection {
adaptive = "Yes"
}
resources := "None"
if len(policy.AttachedResources) > 0 {
resources = strings.Join(policy.AttachedResources, ", ")
}
lootFile.Contents += fmt.Sprintf(
"# %s%s\n"+
"Project: %s | Type: %s\n"+
"Rules: %d | Adaptive Protection: %s\n"+
"Attached Resources: %s\n",
policy.Name, flagStr,
policy.ProjectID, policy.Type,
policy.RuleCount, adaptive,
resources,
)
// Add weaknesses if any
if len(policy.Weaknesses) > 0 {
lootFile.Contents += "Weaknesses:\n"
for _, weakness := range policy.Weaknesses {
lootFile.Contents += fmt.Sprintf(" - %s\n", weakness)
}
}
// Add rules
if len(policy.Rules) > 0 {
lootFile.Contents += "Rules:\n"
for _, rule := range policy.Rules {
preview := ""
if rule.Preview {
preview = " [PREVIEW]"
}
lootFile.Contents += fmt.Sprintf(
" - Priority %d: %s%s\n"+
" Match: %s\n",
rule.Priority, rule.Action, preview,
rule.Match,
)
if rule.RateLimitConfig != nil {
lootFile.Contents += fmt.Sprintf(
" Rate Limit: %d requests per %d seconds\n",
rule.RateLimitConfig.ThresholdCount,
rule.RateLimitConfig.IntervalSec,
)
}
}
}
lootFile.Contents += "\n"
}
func (m *CloudArmorModule) addUnprotectedLBToLoot(projectID, lbName string) {
if lootFile := m.LootMap[projectID]["cloudarmor-details"]; lootFile != nil {
lootFile.Contents += fmt.Sprintf(
"# %s [UNPROTECTED]\n"+
"Project: %s\n"+
"No Cloud Armor policy attached\n\n",
lbName, projectID,
)
}
}
// ------------------------------
// Output Generation
// ------------------------------
@@ -273,11 +166,15 @@ func (m *CloudArmorModule) writeOutput(ctx context.Context, logger internal.Logg
}
func (m *CloudArmorModule) getPoliciesHeader() []string {
return []string{"Project", "Name", "Type", "Rules", "Attached Resources", "Adaptive Protection"}
return []string{"Project", "Policy", "Type", "Rules", "Adaptive", "DDoS", "Attached To", "Weaknesses"}
}
func (m *CloudArmorModule) getRulesHeader() []string {
return []string{"Project", "Policy", "Priority", "Action", "Preview", "Match", "Rate Limit"}
}
func (m *CloudArmorModule) getUnprotectedLBsHeader() []string {
return []string{"Project", "Backend Service"}
return []string{"Project", "Backend Service", "Status"}
}
func (m *CloudArmorModule) policiesToTableBody(policies []cloudarmorservice.SecurityPolicy) [][]string {
@@ -288,29 +185,75 @@ func (m *CloudArmorModule) policiesToTableBody(policies []cloudarmorservice.Secu
adaptive = "Yes"
}
ddos := "-"
if policy.DDOSProtection != "" {
ddos = policy.DDOSProtection
}
resources := "-"
if len(policy.AttachedResources) > 0 {
resources = strings.Join(policy.AttachedResources, ", ")
}
weaknesses := "-"
if len(policy.Weaknesses) > 0 {
weaknesses = strings.Join(policy.Weaknesses, "; ")
}
body = append(body, []string{
m.GetProjectName(policy.ProjectID),
policy.Name,
policy.Type,
fmt.Sprintf("%d", policy.RuleCount),
resources,
adaptive,
ddos,
resources,
weaknesses,
})
}
return body
}
func (m *CloudArmorModule) rulesToTableBody(policies []cloudarmorservice.SecurityPolicy) [][]string {
var body [][]string
for _, policy := range policies {
for _, rule := range policy.Rules {
preview := "No"
if rule.Preview {
preview = "Yes"
}
rateLimit := "-"
if rule.RateLimitConfig != nil {
rateLimit = fmt.Sprintf("%d/%ds", rule.RateLimitConfig.ThresholdCount, rule.RateLimitConfig.IntervalSec)
}
match := rule.Match
if len(match) > 80 {
match = match[:77] + "..."
}
body = append(body, []string{
m.GetProjectName(policy.ProjectID),
policy.Name,
fmt.Sprintf("%d", rule.Priority),
rule.Action,
preview,
match,
rateLimit,
})
}
}
return body
}
func (m *CloudArmorModule) unprotectedLBsToTableBody(projectID string, lbs []string) [][]string {
var body [][]string
for _, lb := range lbs {
body = append(body, []string{
m.GetProjectName(projectID),
lb,
"UNPROTECTED",
})
}
return body
@@ -325,6 +268,16 @@ func (m *CloudArmorModule) buildTablesForProject(projectID string) []internal.Ta
Header: m.getPoliciesHeader(),
Body: m.policiesToTableBody(policies),
})
// Add rules table if there are rules
rulesBody := m.rulesToTableBody(policies)
if len(rulesBody) > 0 {
tableFiles = append(tableFiles, internal.TableFile{
Name: "security-policy-rules",
Header: m.getRulesHeader(),
Body: rulesBody,
})
}
}
if lbs, ok := m.UnprotectedLBs[projectID]; ok && len(lbs) > 0 {
@@ -355,17 +308,7 @@ func (m *CloudArmorModule) writeHierarchicalOutput(ctx context.Context, logger i
for projectID := range projectIDs {
tableFiles := m.buildTablesForProject(projectID)
var lootFiles []internal.LootFile
if projectLoot, ok := m.LootMap[projectID]; ok {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
outputData.ProjectLevelData[projectID] = CloudArmorOutput{Table: tableFiles, Loot: lootFiles}
outputData.ProjectLevelData[projectID] = CloudArmorOutput{Table: tableFiles}
}
pathBuilder := m.BuildPathBuilder()
@@ -388,6 +331,18 @@ func (m *CloudArmorModule) writeFlatOutput(ctx context.Context, logger internal.
})
}
// Add rules table if there are rules
if len(allPolicies) > 0 {
rulesBody := m.rulesToTableBody(allPolicies)
if len(rulesBody) > 0 {
tables = append(tables, internal.TableFile{
Name: "security-policy-rules",
Header: m.getRulesHeader(),
Body: rulesBody,
})
}
}
// Build unprotected LBs table from all projects
var allUnprotectedBody [][]string
for projectID, lbs := range m.UnprotectedLBs {
@@ -401,16 +356,7 @@ func (m *CloudArmorModule) writeFlatOutput(ctx context.Context, logger internal.
})
}
var lootFiles []internal.LootFile
for _, projectLoot := range m.LootMap {
for _, loot := range projectLoot {
if loot != nil && loot.Contents != "" && !strings.HasSuffix(loot.Contents, "# Generated by CloudFox\n\n") {
lootFiles = append(lootFiles, *loot)
}
}
}
output := CloudArmorOutput{Table: tables, Loot: lootFiles}
output := CloudArmorOutput{Table: tables}
scopeNames := make([]string, len(m.ProjectIDs))
for i, id := range m.ProjectIDs {
+1 -1
View File
@@ -347,7 +347,7 @@ func (m *CloudBuildModule) triggersToTableBody(triggers []cloudbuildservice.Trig
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "(default)" && sa != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
+4 -7
View File
@@ -31,7 +31,7 @@ Security Columns:
- Ingress: INGRESS_TRAFFIC_ALL (public), INTERNAL_ONLY, or INTERNAL_LOAD_BALANCER
- Public: Whether allUsers or allAuthenticatedUsers can invoke the service
- Service Account: The identity the service runs as
- SA Attack Paths: Privesc/exfil/lateral movement potential (requires --attack-paths)
- SA Attack Paths: Privesc/exfil/lateral movement potential (run foxmapper first)
- VPC Access: Network connectivity to VPC resources
- Env Vars: Count of plain environment variables
- Secret Mgr: Count of env vars referencing Secret Manager (secure storage)
@@ -44,10 +44,7 @@ Attack Surface:
- Container images may contain vulnerabilities or secrets
- Hardcoded secrets in env vars are a critical security risk
TIP: To see service account attack paths (privesc, exfil, lateral movement),
use the global --attack-paths flag:
cloudfox gcp cloudrun -p PROJECT_ID --attack-paths`,
TIP: Run foxmapper first to populate the SA Attack Paths column.`,
Run: runGCPCloudRunCommand,
}
@@ -469,7 +466,7 @@ func (m *CloudRunModule) buildTablesForProject(projectID string, services []Clou
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if svc.ServiceAccount != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, svc.ServiceAccount)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
@@ -537,7 +534,7 @@ func (m *CloudRunModule) buildTablesForProject(projectID string, services []Clou
}
// Check attack paths (privesc/exfil/lateral) for the service account
jobAttackPaths := "run --attack-paths"
jobAttackPaths := "run foxmapper"
if job.ServiceAccount != "" {
jobAttackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, job.ServiceAccount)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
+1 -1
View File
@@ -201,7 +201,7 @@ func (m *ComposerModule) environmentsToTableBody(environments []composerservice.
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "(default)" && sa != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
+45 -61
View File
@@ -33,27 +33,27 @@ Features:
- Discovers Pub/Sub subscriptions exporting to other projects (BQ, GCS, push)
- Generates exploitation commands for lateral movement
- Highlights service accounts spanning trust boundaries
- Shows impersonation targets when --attack-paths flag is used
- Shows impersonation targets (run foxmapper first for attack path analysis)
RECOMMENDED: For comprehensive cross-project analysis, use with -A and --org-cache
to automatically discover all accessible projects in your organization:
RECOMMENDED: For comprehensive cross-project analysis, use -A to analyze all accessible projects:
cloudfox gcp crossproject -A --org-cache --attack-paths
cloudfox gcp crossproject -A
This will:
- Discover all projects you have access to (--org-cache)
- Analyze cross-project patterns across all of them (-A)
- Include impersonation targets and attack paths (--attack-paths)
- Use cached org/folder/project data (auto-populated, refreshes every 24h)
- Analyze cross-project patterns across all accessible projects
- Show "Trust Boundary" column indicating if target is Internal, External, or Unknown
TRUST BOUNDARY COLUMN (requires --org-cache):
TRUST BOUNDARY COLUMN:
- "Internal" - Target project is within your organization
- "External" - Target project is outside your organization (trust boundary crossing!)
- "Unknown" - Org cache not available, cannot determine boundary
- "Unknown" - Cannot determine boundary
ALTERNATIVE: Specify projects manually with -l for a project list file:
cloudfox gcp crossproject -l projects.txt --attack-paths
cloudfox gcp crossproject -l projects.txt
TIP: Run foxmapper first to populate the Attack Paths column.
WARNING: Requires multiple projects to be specified for effective analysis.
Single project analysis (-p) will have limited results.`,
@@ -124,7 +124,7 @@ func (m *CrossProjectModule) Execute(ctx context.Context, logger internal.Logger
logger.InfoM("Using FoxMapper graph data for attack path analysis", globals.GCP_CROSSPROJECT_MODULE_NAME)
}
// Get org cache from context (populated by --org-cache flag or all-checks)
// Get org cache from context (auto-loaded at startup)
m.OrgCache = gcpinternal.GetOrgCacheFromContext(ctx)
// If no context cache, try loading from disk cache
@@ -234,11 +234,13 @@ func (m *CrossProjectModule) initializeLootFiles() {
}
func (m *CrossProjectModule) addBindingToLoot(binding crossprojectservice.CrossProjectBinding) {
// Add exploitation commands
// Only add if there are exploitation commands
if len(binding.ExploitCommands) > 0 {
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"# IAM Binding: %s -> %s\n# Principal: %s\n# Role: %s\n",
binding.SourceProject, binding.TargetProject, binding.Principal, binding.Role,
"### %s -> %s (%s)\n",
m.GetProjectName(binding.SourceProject),
m.GetProjectName(binding.TargetProject),
cleanRole(binding.Role),
)
for _, cmd := range binding.ExploitCommands {
m.LootMap["crossproject-commands"].Contents += cmd + "\n"
@@ -248,66 +250,40 @@ func (m *CrossProjectModule) addBindingToLoot(binding crossprojectservice.CrossP
}
func (m *CrossProjectModule) addServiceAccountToLoot(sa crossprojectservice.CrossProjectServiceAccount) {
// Add impersonation commands for cross-project SAs
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"# Cross-project SA: %s (Home: %s)\n"+
"gcloud auth print-access-token --impersonate-service-account=%s\n\n",
sa.Email, sa.ProjectID, sa.Email,
)
// Skip - service account cross-project access is covered by bindings and lateral movement paths
// Adding separate impersonation commands would be redundant
}
func (m *CrossProjectModule) addLateralMovementToLoot(path crossprojectservice.LateralMovementPath) {
// Add lateral movement exploitation commands
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"# Lateral Movement: %s -> %s\n"+
"# Principal: %s\n"+
"# Method: %s\n"+
"# Target Roles: %s\n",
path.SourceProject, path.TargetProject,
path.SourcePrincipal,
path.AccessMethod,
strings.Join(path.TargetRoles, ", "),
)
// Only add if there are exploitation commands
if len(path.ExploitCommands) > 0 {
// Clean up role names for display
var cleanedRoles []string
for _, r := range path.TargetRoles {
cleanedRoles = append(cleanedRoles, cleanRole(r))
}
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"### %s -> %s (%s)\n",
m.GetProjectName(path.SourceProject),
m.GetProjectName(path.TargetProject),
strings.Join(cleanedRoles, ", "),
)
for _, cmd := range path.ExploitCommands {
m.LootMap["crossproject-commands"].Contents += cmd + "\n"
}
m.LootMap["crossproject-commands"].Contents += "\n"
}
m.LootMap["crossproject-commands"].Contents += "\n"
}
func (m *CrossProjectModule) addLoggingSinkToLoot(sink crossprojectservice.CrossProjectLoggingSink) {
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"# Cross-Project Logging Sink: %s\n"+
"# Source Project: %s -> Target Project: %s\n"+
"# Destination: %s (%s)\n",
sink.SinkName,
sink.SourceProject, sink.TargetProject,
sink.Destination, sink.DestinationType,
)
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"gcloud logging sinks describe %s --project=%s\n\n",
sink.SinkName, sink.SourceProject,
)
// Logging sinks are data exports, not direct exploitation paths
// Skip adding to loot - the table output is sufficient
}
func (m *CrossProjectModule) addPubSubExportToLoot(export crossprojectservice.CrossProjectPubSubExport) {
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"# Cross-Project Pub/Sub Export: %s\n"+
"# Subscription: %s (Source: %s)\n"+
"# Topic: %s (Project: %s)\n"+
"# Export Type: %s -> Destination: %s\n",
export.SubscriptionName,
export.SubscriptionName, export.SourceProject,
export.TopicName, export.TopicProject,
export.ExportType,
export.ExportDest,
)
m.LootMap["crossproject-commands"].Contents += fmt.Sprintf(
"gcloud pubsub subscriptions describe %s --project=%s\n\n",
export.SubscriptionName, export.SourceProject,
)
// Pub/Sub exports are data exports, not direct exploitation paths
// Skip adding to loot - the table output is sufficient
}
// ------------------------------
@@ -634,6 +610,7 @@ func (m *CrossProjectModule) writeFlatOutput(ctx context.Context, logger interna
lootFiles := m.collectLootFiles()
// Write output for each target project separately
isFirstProject := true
for targetProject, body := range bodyByProject {
if len(body) == 0 {
continue
@@ -647,9 +624,16 @@ func (m *CrossProjectModule) writeFlatOutput(ctx context.Context, logger interna
},
}
// Only include loot files on the first project to avoid duplicate writes
var projectLoot []internal.LootFile
if isFirstProject {
projectLoot = lootFiles
isFirstProject = false
}
output := CrossProjectOutput{
Table: tables,
Loot: lootFiles,
Loot: projectLoot,
}
err := internal.HandleOutputSmart(
+1 -1
View File
@@ -191,7 +191,7 @@ func (m *DataflowModule) jobsToTableBody(jobs []dataflowservice.JobInfo) [][]str
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if job.ServiceAccount != "" && m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, job.ServiceAccount)
} else {
+1 -1
View File
@@ -201,7 +201,7 @@ func (m *DataprocModule) clustersToTableBody(clusters []dataprocservice.ClusterI
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "(default)" && sa != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
+2 -5
View File
@@ -45,10 +45,7 @@ Attack Surface:
- Functions with VPC connectors can access internal resources
- Event triggers reveal integration points (Pub/Sub, Storage, etc.)
TIP: To see service account attack paths (privesc, exfil, lateral movement),
use the global --attack-paths flag:
cloudfox gcp functions -p PROJECT_ID --attack-paths`,
TIP: Run foxmapper first to populate the Attack Paths column with privesc/exfil/lateral movement analysis.`,
Run: runGCPFunctionsCommand,
}
@@ -551,7 +548,7 @@ func (m *FunctionsModule) functionsToTableBody(functions []FunctionsService.Func
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if serviceAccount != "-" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, serviceAccount)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
+1 -1
View File
@@ -391,7 +391,7 @@ func (m *GKEModule) buildTablesForProject(clusters []GKEService.ClusterInfo, nod
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if saDisplay != "-" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, saDisplay)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
+1 -1
View File
@@ -864,7 +864,7 @@ func (m *InstancesModule) instancesToTableBody(instances []ComputeEngineService.
// Check attack paths (privesc/exfil/lateral) for the service account
// FoxMapper takes priority if available (graph-based analysis)
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if saEmail != "-" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, saEmail)
} else if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
+2 -2
View File
@@ -237,7 +237,7 @@ func (m *NotebooksModule) instancesToTableBody(instances []notebooksservice.Note
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "(default)" && sa != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
@@ -292,7 +292,7 @@ func (m *NotebooksModule) runtimesToTableBody(runtimes []notebooksservice.Runtim
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "-" && sa != "" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
+1 -1
View File
@@ -85,7 +85,7 @@ func runGCPOrganizationsCommand(cmd *cobra.Command, args []string) {
func (m *OrganizationsModule) Execute(ctx context.Context, logger internal.Logger) {
orgsSvc := orgsservice.New()
// Check if org cache is available (from all-checks or --org-cache flag)
// Check if org cache is available (auto-loaded at startup)
if orgCache := gcpinternal.GetOrgCacheFromContext(ctx); orgCache != nil && orgCache.IsPopulated() {
logger.InfoM("Using cached organization data", globals.GCP_ORGANIZATIONS_MODULE_NAME)
+1 -1
View File
@@ -265,7 +265,7 @@ func (m *SchedulerModule) jobsToTableBody(jobs []SchedulerService.JobInfo) [][]s
}
// Check attack paths (privesc/exfil/lateral) for the service account
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
if sa != "-" {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, sa)
+2 -3
View File
@@ -39,8 +39,7 @@ Security Considerations:
- Cloud Build SA is a common privilege escalation vector
- Default compute SA often has Editor role
TIP: Use the --attack-paths flag to analyze privesc/exfil/lateral movement potential:
cloudfox gcp service-agents -p PROJECT_ID --attack-paths`,
TIP: Run foxmapper first to populate the Attack Paths column with privesc/exfil/lateral movement analysis.`,
Run: runGCPServiceAgentsCommand,
}
@@ -457,7 +456,7 @@ func (m *ServiceAgentsModule) agentsToTableBody(agents []serviceagentsservice.Se
}
// Check attack paths for this service agent
attackPaths := "run --attack-paths"
attackPaths := "run foxmapper"
if m.FoxMapperCache != nil && m.FoxMapperCache.IsPopulated() {
attackPaths = gcpinternal.GetAttackSummaryFromCaches(m.FoxMapperCache, nil, agent.Email)
}
@@ -356,36 +356,46 @@ func (s *CrossProjectService) analyzeBindingRisk(role, member string, isFromKnow
func (s *CrossProjectService) generateExploitCommands(binding CrossProjectBinding) []string {
var commands []string
// Build impersonation flag if service account
impersonateFlag := ""
if binding.PrincipalType == "serviceAccount" {
email := strings.TrimPrefix(binding.Principal, "serviceAccount:")
commands = append(commands,
fmt.Sprintf("# Impersonate SA from %s to access %s:", binding.SourceProject, binding.TargetProject),
fmt.Sprintf("gcloud auth print-access-token --impersonate-service-account=%s", email),
fmt.Sprintf("# Then use token to access target project:"),
fmt.Sprintf("gcloud projects describe %s --impersonate-service-account=%s", binding.TargetProject, email),
)
impersonateFlag = fmt.Sprintf(" --impersonate-service-account=%s", email)
}
// Role-specific exploitation
if strings.Contains(binding.Role, "storage") {
roleLower := strings.ToLower(binding.Role)
// Role-specific exploitation commands
if strings.Contains(roleLower, "owner") || strings.Contains(roleLower, "editor") {
commands = append(commands,
fmt.Sprintf("# List buckets in target project:"),
fmt.Sprintf("gcloud compute instances list --project=%s%s", binding.TargetProject, impersonateFlag),
fmt.Sprintf("gcloud secrets list --project=%s%s", binding.TargetProject, impersonateFlag),
fmt.Sprintf("gsutil ls -p %s", binding.TargetProject),
)
}
if strings.Contains(binding.Role, "compute") {
} else if strings.Contains(roleLower, "storage") {
commands = append(commands,
fmt.Sprintf("# List instances in target project:"),
fmt.Sprintf("gcloud compute instances list --project=%s", binding.TargetProject),
fmt.Sprintf("gsutil ls -p %s", binding.TargetProject),
)
}
if strings.Contains(binding.Role, "secretmanager") {
} else if strings.Contains(roleLower, "compute") {
commands = append(commands,
fmt.Sprintf("# List secrets in target project:"),
fmt.Sprintf("gcloud secrets list --project=%s", binding.TargetProject),
fmt.Sprintf("gcloud compute instances list --project=%s%s", binding.TargetProject, impersonateFlag),
)
} else if strings.Contains(roleLower, "secretmanager") {
commands = append(commands,
fmt.Sprintf("gcloud secrets list --project=%s%s", binding.TargetProject, impersonateFlag),
)
} else if strings.Contains(roleLower, "bigquery") {
commands = append(commands,
fmt.Sprintf("bq ls --project_id=%s", binding.TargetProject),
fmt.Sprintf("bq query --project_id=%s 'SELECT * FROM INFORMATION_SCHEMA.TABLES'", binding.TargetProject),
)
} else if strings.Contains(roleLower, "cloudsql") {
commands = append(commands,
fmt.Sprintf("gcloud sql instances list --project=%s%s", binding.TargetProject, impersonateFlag),
)
} else if strings.Contains(roleLower, "serviceaccounttokencreator") || strings.Contains(roleLower, "serviceaccountkeyadmin") {
commands = append(commands,
fmt.Sprintf("gcloud iam service-accounts list --project=%s%s", binding.TargetProject, impersonateFlag),
)
}
@@ -396,24 +406,39 @@ func (s *CrossProjectService) generateExploitCommands(binding CrossProjectBindin
func (s *CrossProjectService) generateLateralMovementCommands(path LateralMovementPath) []string {
var commands []string
// Build impersonation flag if service account
impersonateFlag := ""
if strings.HasPrefix(path.SourcePrincipal, "serviceAccount:") {
email := strings.TrimPrefix(path.SourcePrincipal, "serviceAccount:")
impersonateFlag = fmt.Sprintf(" --impersonate-service-account=%s", email)
}
commands = append(commands,
fmt.Sprintf("# Lateral movement from %s to %s via SA impersonation:", path.SourceProject, path.TargetProject),
fmt.Sprintf("# 1. Get access token for the cross-project SA:"),
fmt.Sprintf("gcloud auth print-access-token --impersonate-service-account=%s", email),
fmt.Sprintf("# 2. Use the SA to access target project:"),
)
// Add role-specific commands
for _, role := range path.TargetRoles {
if strings.Contains(role, "owner") || strings.Contains(role, "editor") {
commands = append(commands,
fmt.Sprintf("# Full project access with %s:", role),
fmt.Sprintf("gcloud projects describe %s --impersonate-service-account=%s", path.TargetProject, email),
)
}
// Add role-specific commands based on the most powerful role
for _, role := range path.TargetRoles {
roleLower := strings.ToLower(role)
if strings.Contains(roleLower, "owner") || strings.Contains(roleLower, "editor") {
commands = append(commands,
fmt.Sprintf("gcloud compute instances list --project=%s%s", path.TargetProject, impersonateFlag),
fmt.Sprintf("gcloud secrets list --project=%s%s", path.TargetProject, impersonateFlag),
fmt.Sprintf("gsutil ls -p %s", path.TargetProject),
)
break // owner/editor covers everything, no need for more specific commands
} else if strings.Contains(roleLower, "storage") {
commands = append(commands,
fmt.Sprintf("gsutil ls -p %s", path.TargetProject),
)
} else if strings.Contains(roleLower, "compute") {
commands = append(commands,
fmt.Sprintf("gcloud compute instances list --project=%s%s", path.TargetProject, impersonateFlag),
)
} else if strings.Contains(roleLower, "secretmanager") {
commands = append(commands,
fmt.Sprintf("gcloud secrets list --project=%s%s", path.TargetProject, impersonateFlag),
)
} else if strings.Contains(roleLower, "bigquery") {
commands = append(commands,
fmt.Sprintf("bq ls --project_id=%s", path.TargetProject),
)
}
}
+39 -4
View File
@@ -3,7 +3,10 @@ package gcpinternal
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
foxmapperservice "github.com/BishopFox/cloudfox/gcp/services/foxmapperService"
)
@@ -14,6 +17,8 @@ type FoxMapperCache struct {
service *foxmapperservice.FoxMapperService
populated bool
identifier string
loadedPath string
dataAge time.Duration
}
// NewFoxMapperCache creates a new FoxMapper cache
@@ -132,6 +137,26 @@ func (c *FoxMapperCache) GetIdentifier() string {
return c.identifier
}
// GetDataAge returns how old the FoxMapper data is
func (c *FoxMapperCache) GetDataAge() time.Duration {
return c.dataAge
}
// GetDataAgeDays returns the age of FoxMapper data in days
func (c *FoxMapperCache) GetDataAgeDays() int {
return int(c.dataAge.Hours() / 24)
}
// SetLoadedPath sets the path and calculates data age from file modification time
func (c *FoxMapperCache) SetLoadedPath(path string) {
c.loadedPath = path
// Try to get the modification time of the nodes.json file
nodesPath := filepath.Join(path, "graph", "nodes.json")
if info, err := os.Stat(nodesPath); err == nil {
c.dataAge = time.Since(info.ModTime())
}
}
// HasPrivesc checks if a service account has privilege escalation potential
func (c *FoxMapperCache) HasPrivesc(serviceAccount string) (bool, string) {
if !c.populated {
@@ -182,18 +207,25 @@ func TryLoadFoxMapper(orgID string, projectIDs []string) *FoxMapperCache {
// Try org first - if it exists, it should contain all projects
if orgID != "" {
if err := cache.LoadFromOrg(orgID); err == nil {
return cache
if path, err := foxmapperservice.FindFoxMapperData(orgID, true); err == nil {
if err := cache.LoadFromOrg(orgID); err == nil {
cache.SetLoadedPath(path)
return cache
}
}
}
// No org-level graph - try to load and merge all project graphs
loadedCount := 0
var firstPath string
for _, projectID := range projectIDs {
if loadedCount == 0 {
// First project - load normally
if err := cache.LoadFromProject(projectID); err == nil {
loadedCount++
if path, err := foxmapperservice.FindFoxMapperData(projectID, false); err == nil {
if err := cache.LoadFromProject(projectID); err == nil {
firstPath = path
loadedCount++
}
}
} else {
// Subsequent projects - merge into existing graph
@@ -213,6 +245,9 @@ func TryLoadFoxMapper(orgID string, projectIDs []string) *FoxMapperCache {
}
if loadedCount > 0 {
if firstPath != "" {
cache.SetLoadedPath(firstPath)
}
return cache
}
+12
View File
@@ -67,6 +67,18 @@ func (l *Logger) SuccessM(text string, module string) {
fmt.Printf(clearln+"[%s][%s] %s\n", green(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), green(module), text)
}
func (l *Logger) Warn(text string) {
l.WarnM(text, "config")
}
func (l *Logger) WarnM(text string, module string) {
var yellow = color.New(color.FgYellow).SprintFunc()
fmt.Printf(clearln+"[%s][%s] ⚠️ %s\n", yellow(emoji.Sprintf(":fox:cloudfox %s :fox:", l.version)), yellow(module), text)
if l.txtLog != nil {
l.txtLog.Printf("[%s] WARNING: %s", module, text)
}
}
func (l *Logger) Error(text string) {
l.ErrorM(text, "config")
}